TypeScript知识梳理 学习目录

基础

类型声明空间与变量声明空间

在TypeScript中,有两个相关的概念:类型声明空间(Type Declaration Space)和变量声明空间(Variable Declaration Space)。

类型声明空间是指用于定义类型的命名空间,它包含了在代码中定义的类型、接口、类、枚举等。在类型声明空间中,你可以使用interfaceclasstype等关键字来定义类型,并通过命名空间来组织和管理这些类型的定义。

例如,下面的代码中,Person接口和Car类都是在类型声明空间中定义的:

namespace MyNamespace {
  interface Person {
    name: string;
    age: number;
  }

  class Car {
    brand: string;
    color: string;
  }
}

变量声明空间是指用于定义变量的命名空间,它包含了在代码中定义的变量、常量、函数等。在变量声明空间中,你可以使用letconstfunction等关键字来定义变量和函数,并通过命名空间来组织和管理这些变量的定义。

例如,下面的代码中,name变量和sayHello函数都是在变量声明空间中定义的:

namespace MyNamespace {
  let name: string = "John";

  function sayHello() {
    console.log("Hello, " + name);
  }
}

需要注意的是,类型声明空间和变量声明空间是相互独立的,它们可以存在于同一个命名空间中,也可以分别存在于不同的命名空间中。在命名空间中,你可以同时定义类型和变量,以及它们之间的关联关系。

使用命名空间可以帮助组织和管理代码,避免命名冲突,并提供更好的代码可读性和可维护性。在TypeScript中,你可以使用命名空间来创建模块化的代码结构,并将相关的类型和变量组织在一起。

命名空间(Namespace)是 TypeScript 的语法特性之一。TypeScript 引入了命名空间的概念,用于组织和管理代码,防止命名冲突,并实现模块化的结构。

在 TypeScript 中,命名空间通过关键字 namespace 来定义。可以使用命名空间来封装变量、函数、类和其他命名空间,以创建更结构化和可维护的代码。

下面是一个使用命名空间的 TypeScript 示例:

namespace MyNamespace {
  // 命名空间内的变量和函数
  export const name: string = "John";

  export function sayHello() {
    console.log("Hello, " + name);
  }
}

// 使用命名空间中的成员
MyNamespace.sayHello();

在上述示例中,我们创建了一个名为 MyNamespace 的命名空间,其中包含了变量 name 和函数 sayHello。通过 export 关键字,我们可以将命名空间内的成员暴露给外部使用。

请注意,TypeScript 还提供了模块化的语法,即使用 importexport 关键字来实现模块化的组织和导入导出功能。模块化是推荐的代码组织方式,而命名空间主要用于旧版 JavaScript 代码的命名空间封装和兼容性。

类型注解与类型推断

在 TypeScript 中,有两种主要的方式来定义类型:类型注解(Type Annotations)和类型推断(Type Inference)。

  1. 类型注解:类型注解是显式地为变量、函数参数、函数返回值等添加类型信息的方式。通过使用冒号(:)后跟类型,我们可以告诉 TypeScript 变量的预期类型。例如:
let age: number = 25;
function add(a: number, b: number): number {
  return a + b;
}

在上述示例中,我们使用类型注解为 age 变量指定了 number 类型,以及为 add 函数的参数和返回值指定了 number 类型。

类型注解可以提供更明确的类型信息,有助于在开发过程中捕获潜在的类型错误,并提供更好的代码提示和类型检查。

  1. 类型推断:类型推断是 TypeScript 的一项功能,它可以根据变量的初始值或上下文推断出变量的类型,而无需显式地添加类型注解。例如:
let age = 25; // TypeScript 推断 age 的类型为 number
const name = "John"; // TypeScript 推断 name 的类型为 "John" 字面量类型

在上述示例中,我们没有显式地添加类型注解,而是通过变量的初始值让 TypeScript 推断出变量的类型。

类型推断可以简化代码,减少冗余的类型注解,但有时可能无法准确推断出类型,特别是在复杂的情况下。在这种情况下,可以使用类型注解来明确指定类型,提供更准确的类型信息。

总的来说,类型注解和类型推断是 TypeScript 中用于定义类型的两种方式。类型注解提供了明确的类型信息,而类型推断则根据上下文自动推断类型。可以根据具体的情况选择使用其中一种或两种方式来定义类型。

类型分类与联合类型与交叉类型

在 TypeScript 中,有几种类型分类,包括原始类型、对象类型、数组类型、函数类型和特殊类型。下面我们来看一下这些类型分类以及 TypeScript 中的联合类型和交叉类型。

  1. 原始类型:包括 numberstringbooleannullundefinedsymbol。这些类型是 TypeScript 中最基本的类型,表示数字、字符串、布尔值、空值和唯一标识符。

  2. 对象类型:包括对象字面量类型、类类型、接口类型等。对象类型表示具有特定属性和方法的对象。

  3. 数组类型:表示由相同类型的元素组成的数组。可以使用泛型数组类型 Array<ElementType> 或元素类型后跟 [] 表示数组类型。

  4. 函数类型:表示函数的类型,包括函数的参数类型和返回值类型。可以使用箭头函数类型 (params: Type) => ReturnType 或函数类型 (params: Type) => ReturnType 来表示函数类型。

  5. 特殊类型:包括 anyvoidneverunknownany 表示任意类型,void 表示没有返回值的类型,never 表示永远不会有返回值的类型,unknown 表示未知类型。

接下来,我们来看一下联合类型和交叉类型。

  • 联合类型(Union Types):联合类型表示一个值可以是多个类型之一。使用 | 符号将多个类型组合在一起。例如,number | string 表示一个值可以是数字或字符串类型。
function printId(id: number | string) {
  console.log(id);
}

printId(123); // 正确
printId("abc"); // 正确
printId(true); // 错误,参数类型必须是 number 或 string
  • 交叉类型(Intersection Types):交叉类型表示一个值具有多个类型的特征。使用 & 符号将多个类型组合在一起。例如,Type1 & Type2 表示一个值具有 Type1 和 Type2 的属性和方法。
interface Walkable {
  walk(): void;
}

interface Swimmable {
  swim(): void;
}

function action(animal: Walkable & Swimmable) {
  animal.walk();
  animal.swim();
}

const person = {
  walk() {
    console.log("Walking");
  },
  swim() {
    console.log("Swimming");
  },
};

action(person); // 正确,person 满足 Walkable 和 Swimmable 接口的要求

在上述示例中,我们定义了 WalkableSwimmable 两个接口,然后定义了一个函数 action,它接受一个同时满足 WalkableSwimmable 接口要求的对象作为参数。

总结一下,联合类型允许一个值具有多个可能的类型,而交叉类型允许一个值具有多个类型的特征。这些类型可以帮助我们更灵活地定义变量、函数参数和函数返回值的类型。

never类型、any类型、unknown类型

在 TypeScript 中,有一些特殊的类型,包括 neveranyunknown

  1. never 类型:never 类型表示那些永远不会发生的值的类型。它通常用于表示抛出异常、无法正常结束或永远不会返回的函数的返回类型。
function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {
    // 无限循环
  }
}

在上述示例中,throwError 函数抛出一个错误并永远不会返回,因此它的返回类型是 neverinfiniteLoop 函数是一个无限循环,也永远不会返回,因此它的返回类型也是 never

  1. any 类型:any 类型表示任意类型。当一个变量被指定为 any 类型时,它可以接受任何类型的值,而不进行类型检查和推断。
let data: any = 123;
data = "abc";
data = true;

在上述示例中,data 变量被指定为 any 类型,因此它可以被赋值为数字、字符串和布尔值等任意类型的值。

使用 any 类型可以绕过 TypeScript 的类型检查机制,但也会失去类型安全性和编译时的类型检查。

  1. unknown 类型:unknown 类型表示未知类型。与 any 类型不同,unknown 类型要求进行类型检查和类型断言才能使用。
let value: unknown = 123;

if (typeof value === "number") {
  let numberValue: number = value; // 错误,需要进行类型断言
}

let stringValue: string = value as string; // 正确,使用类型断言将 unknown 类型转换为 string 类型

在上述示例中,我们将一个值赋给 unknown 类型的变量 value,然后在 typeof 检查中确定它的类型。如果要将 unknown 类型的值赋给其他类型的变量,需要进行类型断言。

unknown 类型提供了类型安全性,因为它要求进行类型检查和类型断言,避免了直接使用 any 类型可能引发的潜在错误。

总结一下,never 类型表示永远不会发生的值的类型,any 类型表示任意类型,而 unknown 类型表示未知类型。这些特殊类型在某些情况下可以提供灵活性和便利性,但也需要谨慎使用,以保持类型安全性和可维护性。

类型断言与非空断言

在 TypeScript 中,类型断言和非空断言是两种不同的断言操作。

  1. 类型断言(Type Assertion):类型断言用于告诉编译器某个值的具体类型,即手动指定一个变量或表达式的类型。类型断言有两种形式:尖括号语法和 as 语法。

尖括号语法:

let value: any = "Hello, TypeScript!";
let length: number = (<string>value).length;

as 语法:

let value: any = "Hello, TypeScript!";
let length: number = (value as string).length;

在上述示例中,我们将一个值赋给 value 变量,并使用类型断言将其指定为 string 类型。然后,我们可以使用 .length 属性来获取字符串的长度。

类型断言的作用是在编译时告诉编译器某个值的类型,但它不会对值进行运行时的转换或检查。因此,需要确保类型断言的准确性,否则可能导致运行时错误。

  1. 非空断言(Non-null Assertion):非空断言用于告诉编译器一个表达式的结果不会为 nullundefined,即断言一个值是非空的。非空断言使用 ! 符号。
let element: HTMLElement | null = document.getElementById("myElement");
element!.textContent = "Hello, TypeScript!";

在上述示例中,我们使用 document.getElementById 方法获取一个元素,并将其赋给 element 变量。由于 getElementById 方法的返回类型是 HTMLElement | null,存在可能为 null 的情况。但是,我们使用非空断言 ! 来断言 element 不为空,并直接访问它的 textContent 属性。

非空断言的作用是告诉编译器我们知道某个表达式的结果不会为 nullundefined,从而避免在后续使用中出现空值错误。但需要注意,如果实际上该值为 nullundefined,会导致运行时错误。

需要谨慎使用非空断言,确保断言的准确性,避免潜在的空值错误。

数组类型与元组类型

在 TypeScript 中,数组类型和元组类型是两种不同的数据类型。

  1. 数组类型(Array Type):数组类型表示包含相同类型元素的有序集合。在 TypeScript 中,可以使用以下两种方式来声明数组类型:
// 方式一:类型后缀[]
let numbers: number[] = [1, 2, 3, 4, 5];

// 方式二:Array<类型>
let names: Array<string> = ["Alice", "Bob", "Charlie"];

在上述示例中,numbers 是一个包含数字类型元素的数组,names 是一个包含字符串类型元素的数组。

数组类型允许使用数组相关的方法和属性,例如 push()pop()length 等。

  1. 元组类型(Tuple Type):元组类型表示一个固定长度和特定类型顺序的数组。在 TypeScript 中,可以使用以下方式来声明元组类型:
let person: [string, number] = ["Alice", 25];

在上述示例中,person 是一个包含两个元素的元组,第一个元素是字符串类型,第二个元素是数字类型。

元组类型允许通过索引访问特定位置的元素,并且对于每个位置的元素,可以指定其特定的类型。

需要注意的是,元组类型和数组类型有所不同。元组类型具有固定的长度和特定的类型顺序,而数组类型只要求元素具有相同的类型。

总结一下,数组类型表示包含相同类型元素的有序集合,而元组类型表示固定长度和特定类型顺序的数组。

对象类型与索引签名

在 TypeScript 中,对象类型和索引签名是用于描述对象结构的重要概念。

  1. 对象类型(Object Type):对象类型表示具有一组属性和对应类型的集合。可以使用以下方式来声明对象类型:
// 使用字面量形式声明对象类型
let person: { name: string; age: number } = {
  name: "Alice",
  age: 25,
};

// 使用接口声明对象类型
interface Person {
  name: string;
  age: number;
}

let person: Person = {
  name: "Alice",
  age: 25,
};

在上述示例中,我们声明了一个 person 对象,它具有 nameage 两个属性,分别对应字符串类型和数字类型。

对象类型允许使用点符号或方括号来访问对象的属性和方法,例如 person.nameperson["age"]

  1. 索引签名(Index Signature):索引签名用于描述对象类型中可以使用的索引类型和对应的值类型。可以使用以下方式来声明索引签名:
interface Dictionary {
  [key: string]: number;
}

let scores: Dictionary = {
  math: 90,
  english: 85,
  history: 95,
};

在上述示例中,我们声明了一个 Dictionary 接口,它具有一个索引签名 [key: string]: number,表示可以使用字符串类型的键来访问对应的数字类型的值。

索引签名允许我们通过方括号语法来访问对象的属性,例如 scores["math"]

使用索引签名可以实现类似字典或映射的数据结构,其中键是动态的字符串或数字。

函数类型与void类型

在 TypeScript 中,函数类型和 void 类型是两种常见的类型。

  1. 函数类型(Function Type):函数类型用于表示函数的类型。在 TypeScript 中,可以使用以下方式来声明函数类型:
// 声明函数类型
type MyFunctionType = (arg1: number, arg2: string) => boolean;

// 使用函数类型
const myFunction: MyFunctionType = (num, str) => {
  // 函数体
  return true;
};

在上述示例中,MyFunctionType 是一个函数类型,它接受一个 number 类型参数和一个 string 类型参数,并返回一个 boolean 类型的值。然后,我们可以使用 myFunction 来声明一个符合该函数类型的具体函数。

函数类型可以用于声明函数变量、函数参数和函数返回值的类型。

  1. void 类型:void 类型表示函数没有返回值。在 TypeScript 中,可以使用 void 关键字来指定函数的返回类型为 void
function myFunction(): void {
  // 函数体
}

在上述示例中,myFunction 是一个没有返回值的函数,因此其返回类型为 void

void 类型主要用于表示不需要返回值的函数或者表示函数的返回值不被使用。

函数重载与可调用注解

函数重载(Function Overloads)和可调用注解(Callable Annotations)是 TypeScript 中用于描述函数多态性和类型推断的重要特性。

  1. 函数重载:函数重载允许我们为同一个函数提供多个不同的函数签名,以便在不同的参数组合下有不同的行为。通过函数重载,我们可以为函数提供多个函数定义,编译器会根据传入的参数类型选择合适的函数定义。以下是函数重载的示例:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  return a + b;
}

const result1 = add(1, 2); // 类型推断为 number,结果为 3
const result2 = add("Hello, ", "TypeScript"); // 类型推断为 string,结果为 "Hello, TypeScript"

在上述示例中,add 函数有两个重载定义,一个接收两个 number 类型的参数并返回 number 类型的结果,另一个接收两个 string 类型的参数并返回 string 类型的结果。编译器会根据传入的参数类型选择合适的重载定义。

  1. 可调用注解:可调用注解是一种特殊的类型注解,用于描述一个对象是否可被调用。在 TypeScript 中,我们可以使用 CallableFunction 类型注解来表示一个对象是可被调用的。以下是可调用注解的示例:
type MyFunction = {
  (a: number, b: number): number;
};

const add: MyFunction = (a, b) => a + b;

const result = add(1, 2); // 类型推断为 number,结果为 3

在上述示例中,我们使用 MyFunction 类型注解来声明 add 对象是一个可被调用的函数,它接收两个 number 类型的参数并返回 number 类型的结果。通过这种方式,我们可以对函数对象进行类型检查和类型推断。

枚举类型与const枚举

在 TypeScript 中,枚举类型(Enum Type)和 const 枚举是用于表示一组相关常量的两种不同方式。

  1. 枚举类型:枚举类型用于定义一组命名的常量值。通过枚举类型,我们可以为一组相关的值分配有意义的名称。以下是枚举类型的示例:
enum Color {
  Red,
  Green,
  Blue,
}

let myColor: Color = Color.Green;
console.log(myColor); // 输出 1

在上述示例中,Color 是一个枚举类型,它定义了三个常量值 RedGreenBlue。默认情况下,枚举值从 0 开始自动递增。我们可以将枚举值赋给变量 myColor,并打印它的值。

枚举类型提供了一种更具可读性和可维护性的方式来表示一组相关的常量值。

  1. const 枚举:const 枚举是一种更轻量级的枚举,它在编译阶段会被完全删除,并且不能包含计算成员。使用 const 修饰符可以将枚举标记为 const 枚举。以下是 const 枚举的示例:
const enum Color {
  Red,
  Green,
  Blue,
}

let myColor: Color = Color.Green;
console.log(myColor); // 输出 1

在上述示例中,Color 被标记为 const 枚举。与普通枚举不同的是,const 枚举在编译后不会生成实际的 JavaScript 对象,而是直接使用相应的常量值。

const 枚举适用于那些不需要在运行时进行枚举值反查的情况,并且可以减少生成的 JavaScript 代码的大小。

需要注意的是,const 枚举只能使用常量枚举表达式作为枚举的成员,而普通枚举可以使用任意表达式作为枚举的成员。

进阶

接口与类型别名区别

在 TypeScript 中,接口(Interfaces)和类型别名(Type Aliases)都是用于定义自定义类型的工具,但它们有一些区别。

  1. 接口(Interfaces):
    • 接口是一种用于描述对象的结构或类的形状的方式。
    • 接口可以定义属性、方法、索引签名等。
    • 接口可以被类实现(implements)。
    • 接口可以被其他接口继承(extends)。
    • 接口可以用于类型注解、类型断言和类型推断。
    • 接口可以用于声明函数类型。
    • 接口可以被扩展和合并。

以下是接口的示例:

interface Person {
  name: string;
  age: number;
}

const person: Person = {
  name: "John",
  age: 25,
};

在上述示例中,我们定义了一个 Person 接口,它描述了一个拥有 nameage 属性的对象。然后,我们使用 Person 接口来注解 person 变量,确保它符合 Person 接口定义的结构。

  1. 类型别名(Type Aliases):
    • 类型别名是为现有类型创建一个新名称,可以给复杂的类型起一个简洁的别名。
    • 类型别名可以使用联合类型、交叉类型、字面量类型等。
    • 类型别名可以使用泛型。
    • 类型别名不能被类实现或继承。
    • 类型别名可以用于类型注解、类型断言和类型推断。
    • 类型别名不能用于声明函数类型。
    • 类型别名不能被扩展和合并。

以下是类型别名的示例:

type Point = {
  x: number;
  y: number;
};

const point: Point = {
  x: 10,
  y: 20,
};

在上述示例中,我们使用 type 关键字创建了一个类型别名 Point,它表示一个具有 xy 属性的对象。然后,我们使用 Point 类型别名来注解 point 变量,确保它符合 Point 类型别名定义的结构。

总结:

  • 接口适用于描述对象的结构或类的形状,可以被实现和继承,也可以用于声明函数类型。
  • 类型别名适用于为现有类型创建别名,可以使用更简洁的名称来表示复杂的类型,但不能被实现和继承,也不能用于声明函数类型。
  • 在大多数情况下,接口和类型别名可以互相替代使用,选择使用哪种方式取决于具体的需求和个人偏好。

字面量类型与keyof关键字

字面量类型和 keyof 关键字是 TypeScript 中用于创建更精确和灵活的类型的工具。

  1. 字面量类型:
    字面量类型允许我们将特定的值作为类型。它可以是字符串、数字、布尔值或其他字面量。示例如下:
let status: "success" | "error";
status = "success"; // 合法
status = "pending"; // 不合法,只能是 "success" 或 "error"

在上面的示例中,我们定义了一个 status 变量,它的类型是 "success""error"。这意味着 status 只能被赋值为这两个字面量中的一个。

  1. keyof 关键字:
    keyof 关键字用于获取类型的所有属性名组成的联合类型。它可以用于访问对象的属性或类的成员。示例如下:
interface Person {
  name: string;
  age: number;
}

type PersonKeys = keyof Person;
// PersonKeys 的类型为 "name" | "age"

在上面的示例中,我们使用 keyof 关键字获取了 Person 接口的属性名的联合类型。PersonKeys 的类型为 "name" | "age",这意味着它只能是这两个属性名中的一个。

keyof 关键字在许多场景中非常有用,例如用于访问对象属性的值、动态访问类的成员等。

通过使用字面量类型和 keyof 关键字,我们可以创建更精确和灵活的类型,从而提高代码的可靠性和可读性。

类型保护与自定义类型保护

在 TypeScript 中,类型保护(Type Guards)是一种用于在运行时检查变量的类型的技术,以便在不同的类型下执行不同的操作。它可以帮助我们在使用联合类型、类型断言或类型守卫时更准确地确定变量的类型。

有几种类型保护的方式,包括 typeof 类型保护、instanceof 类型保护、in 类型保护和自定义类型保护。

自定义类型保护是一种用户定义的函数,用于确定变量的具体类型。它基于一些条件逻辑来判断变量的类型,并返回一个类型谓词,告诉编译器变量的类型。通过自定义类型保护,我们可以在函数内部对变量进行更复杂的类型检查,以便进行更精确的类型推断。

下面是一个使用自定义类型保护的示例:

interface Cat {
  name: string;
  meow(): void;
}

interface Dog {
  name: string;
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return 'meow' in animal;
}

function makeSound(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow();
  } else {
    animal.bark();
  }
}

在上面的例子中,isCat 是一个自定义类型保护函数。它检查传入的参数是否有 meow 属性,如果有则返回 true,表示变量是 Cat 类型。在 makeSound 函数中,我们使用了自定义类型保护来确定 animal 的具体类型,从而调用相应的方法。

通过自定义类型保护,我们可以在 TypeScript 中更精确地处理联合类型,避免使用类型断言或类型守卫时的不确定性。

定义泛型和泛型常见操作

泛型(Generics)是 TypeScript 中的一种特性,它允许我们在定义函数、类或接口时使用类型参数,以便在使用时指定具体的类型。泛型提供了更灵活和可重用的代码编写方式,可以增加代码的类型安全性和可读性。

以下是定义泛型和泛型常见操作的示例:

  1. 定义泛型函数:
function identity<T>(arg: T): T {
  return arg;
}

// 使用泛型函数
let result = identity<string>("Hello");
console.log(result); // 输出: Hello

// 类型推断,可以省略类型参数
let result2 = identity("World");
console.log(result2); // 输出: World
  1. 泛型约束:
interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// 使用泛型约束的函数
let result = loggingIdentity("Hello");
console.log(result); // 输出: Hello
  1. 泛型类:
class Queue<T> {
  private elements: T[] = [];

  enqueue(element: T): void {
    this.elements.push(element);
  }

  dequeue(): T | undefined {
    return this.elements.shift();
  }
}

// 使用泛型类
let queue = new Queue<number>();
queue.enqueue(1);
queue.enqueue(2);
console.log(queue.dequeue()); // 输出: 1
  1. 泛型接口:
interface Comparator<T> {
  compare(a: T, b: T): number;
}

// 使用泛型接口
class NumberComparator implements Comparator<number> {
  compare(a: number, b: number): number {
    return a - b;
  }
}
  1. 泛型约束与 keyof:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

let person = { name: "Alice", age: 25 };
let name = getProperty(person, "name");
console.log(name); // 输出: Alice

这些是一些常见的泛型操作,泛型还可以与其他类型特性(如联合类型、交叉类型、映射类型等)结合使用,以实现更复杂的类型操作和转换。

类型兼容性详解

类型兼容性是 TypeScript 中的一个重要概念,它描述了在赋值操作或函数参数传递时,一个类型是否能够被另一个类型所接受。TypeScript 的类型系统基于结构类型(structural typing),而不是名义类型(nominal typing),这意味着类型兼容性是基于类型的结构而不是名称来判断的。

在 TypeScript 中,以下规则适用于类型兼容性:

  1. 对象类型的兼容性:

    • 如果目标类型(目标)具有源类型(源)中的所有属性,并且属性的类型兼容,则源类型可以赋值给目标类型。
    • 目标类型可以有额外的属性,但源类型不能有额外的属性。
    • 可选属性在目标类型中可以不存在,但如果存在,其类型必须兼容。
  2. 函数类型的兼容性:

    • 函数参数列表的兼容性:源函数的每个参数类型必须兼容于目标函数的对应参数类型。
    • 函数返回值的兼容性:源函数的返回值类型必须兼容于目标函数的返回值类型。
  3. 函数参数的双向协变:

    • 如果函数参数类型是可选的(可传入 undefined 或省略),则它与相应的必需参数类型兼容。
    • 如果函数参数类型是固定的(不可传入 undefined),则它与相应的可选参数类型兼容。
  4. 函数重载的兼容性:

    • 目标函数的重载列表必须与源函数的重载列表兼容。
  5. 泛型类型的兼容性:

    • 如果泛型类型参数没有显式的类型约束,它可以匹配任何类型。
    • 如果泛型类型参数具有类型约束,它必须兼容于目标类型参数的类型约束。

这些规则使得 TypeScript 的类型系统更灵活,可以进行更精确的类型推断和类型检查,提高代码的安全性和可靠性。

以下是一些类型兼容性的示例:

interface Animal {
  name: string;
}

interface Cat extends Animal {
  meow(): void;
}

let animal: Animal;
let cat: Cat;

animal = cat; // 兼容性:Cat 可以赋值给 Animal
cat = animal; // 错误:Animal 不兼容于 Cat

function printAnimalName(animal: Animal) {
  console.log(animal.name);
}

printAnimalName(cat); // 兼容性:Cat 可以传递给 Animal 参数

function createAnimal(): Animal {
  return { name: "Tom" };
}

cat = createAnimal(); // 错误:Animal 不兼容于 Cat

在上述示例中,Cat 接口继承自 Animal 接口,因此 Cat 类型的变量可以赋值给 Animal 类型的变量。但反过来,Animal 类型的变量不能赋值给 Cat 类型的变量。函数 printAnimalName 接受 Animal 类型的参数,因此可以传递 cat 变量作为参数。但函数 createAnimal 返回 Animal 类型的值,不能直接赋值给 cat 变量。

这些例子展示了 TypeScript 中类型兼容性的一些常见情况,但还有更多细节和规则可供深入学习和探索。

映射类型与内置工具类型

在 TypeScript 中,有一些内置的工具类型和映射类型,可以帮助我们进行类型转换、操作和映射。下面介绍一些常见的内置工具类型和映射类型:

  1. Partial:将类型 T 的所有属性变为可选属性。
interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;
// 等同于
// type PartialPerson = {
//   name?: string;
//   age?: number;
// }
  1. Required:将类型 T 的所有属性变为必选属性。
interface Person {
  name?: string;
  age?: number;
}

type RequiredPerson = Required<Person>;
// 等同于
// type RequiredPerson = {
//   name: string;
//   age: number;
// }
  1. Readonly:将类型 T 的所有属性变为只读属性。
interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;
// 等同于
// type ReadonlyPerson = {
//   readonly name: string;
//   readonly age: number;
// }
  1. Pick<T, K>:从类型 T 中选取部分属性 K 构成新的类型。
interface Person {
  name: string;
  age: number;
  address: string;
}

type PersonInfo = Pick<Person, 'name' | 'age'>;
// 等同于
// type PersonInfo = {
//   name: string;
//   age: number;
// }
  1. Omit<T, K>:从类型 T 中排除部分属性 K 构成新的类型。
interface Person {
  name: string;
  age: number;
  address: string;
}

type PersonWithoutAddress = Omit<Person, 'address'>;
// 等同于
// type PersonWithoutAddress = {
//   name: string;
//   age: number;
// }

这些是 TypeScript 中一些常见的内置工具类型和映射类型,它们可以帮助我们更方便地操作和转换类型。此外,还有其他一些工具类型,如 ReadonlyArray、Record、Exclude、Extract、NonNullable 等,可以根据具体需求选择使用。

条件类型和infer关键字

条件类型和 infer 关键字是 TypeScript 中用于创建更复杂类型的强大工具。条件类型允许我们根据类型的条件进行类型推断,并根据不同的条件返回不同的类型。

下面是一个使用条件类型和 infer 关键字的示例:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function sum(a: number, b: number): number {
  return a + b;
}

type SumReturnType = ReturnType<typeof sum>;
// SumReturnType 的类型为 number,因为 typeof sum 的类型为 (a: number, b: number) => number

在上面的示例中,我们定义了一个 ReturnType 类型,它接受一个泛型参数 T。通过条件类型 T extends (...args: any[]) => infer R,我们检查 T 是否是一个函数类型,并使用 infer 关键字来推断函数的返回类型为 R。如果 T 是一个函数类型,则返回 R,否则返回 never

在使用时,我们传入一个函数 sum 的类型 typeof sumReturnType,并将返回的类型赋值给 SumReturnType。由于 sum 的类型为 (a: number, b: number) => number,所以 SumReturnType 的类型为 number

通过使用条件类型和 infer 关键字,我们可以根据类型的条件进行类型推断,从而创建更灵活和复杂的类型转换和操作。

类中如何使用类型

在 TypeScript 中,我们可以在类中使用类型来定义类的属性、方法、参数和返回值的类型。下面是一些示例,展示了如何在类中使用类型:

  1. 定义类属性的类型:
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

在上面的示例中,我们使用 string 类型和 number 类型分别定义了 nameage 属性的类型。

  1. 定义类方法的参数和返回值的类型:
class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
}

在上面的示例中,我们使用 number 类型定义了 add 方法的参数 ab 的类型,并使用 number 类型定义了方法的返回值类型。

  1. 使用接口定义类的属性和方法的类型:
interface Animal {
  name: string;
  sound(): void;
}

class Dog implements Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  sound(): void {
    console.log("Woof!");
  }
}

在上面的示例中,我们使用接口 Animal 定义了类 Dog 的属性和方法的类型。Dog 类实现了 Animal 接口,并且必须实现接口中定义的属性和方法。

通过使用类型,我们可以在类中明确指定属性、方法、参数和返回值的类型,从而提供类型安全性和代码提示。这有助于在开发过程中捕获潜在的错误,并提高代码的可读性和可维护性。

posted @ 2023-11-03 11:36  Laravel自学开发  阅读(17)  评论(0编辑  收藏  举报