你不知道的 TypeScript 泛型

时间:2022-12-05 13:40:19

你不知道的 TypeScript 泛型

一、泛型是什么

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

泛型表示泛指某一种类型,开发者可以指定一个表示类型的变量,用它来作为实际类型的占位符,用尖括号来包裹类型变量 。泛型的主要作用是创建可重用的组件,从而让一个组件可以支持多种数据类型,它可以作用在接口、类、函数或类型别名上。

下面我们来举个例子,帮助大家更好地理解上述的内容。在这个例子中,我们将一步步揭示泛型的作用。

1.1 identity 函数示例

首先我们来定义一个通用的 identity 函数,该函数接收一个参数并直接返回它:

  1. function identity (value) { 
  2.   return value; 
  3.  
  4. console.log(identity(1)) // 1 

现在,我们将 identity 函数做适当的调整,以支持 TypeScript 的 number 类型的参数:

  1. function identity (value: number) : number { 
  2.  
  3. return value; 
  4.  
  5.  
  6. console.log(identity(1)) // 1 

这里 identity 的问题是我们将 number 类型分配给参数和返回类型,使该函数仅可用于该原始类型。但该函数并不是通用的,很明显这并不是我们所希望的。虽然我们可以把 number 换成 any,但这样的话,我们失去了定义应该返回哪种类型的能力,并且在这个过程中使编译器失去了类型保护的作用。

我们的目标是让 identity 函数可以适用于任意的类型,为了实现这个目标,我们可以使用泛型,具体实现方式如下:

  1. function identity <T>(value: T) : T { 
  2.   return value; 
  3.  
  4. console.log(identity<number>(1)) // 1 

对于刚接触 TypeScript 泛型的读者来说,首次看到 语法会感到陌生。但这没什么可担心的,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。

你不知道的 TypeScript 泛型

参考上面的图片,当我们调用 identity(1) ,number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 number 类型。

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

  1. function identity <T, U>(value: T, message: U) : T { 
  2.   console.log(message); 
  3.   return value; 
  4.  
  5. console.log(identity<Number, string>(68, "Semlinker")); 

你不知道的 TypeScript 泛型

除了为类型变量显式设定值之外,另一种方式是让编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

  1. function identity <T, U>(value: T, message: U) : T { 
  2.   console.log(message); 
  3.   return value; 
  4.  
  5. console.log(identity(68, "Semlinker")); 

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。下面我们来看张动图,直观地感受一下类型传递的过程:

你不知道的 TypeScript 泛型

如你所见,该函数接收你传递给它的任何类型,使得我们可以为不同类型创建可重用的组件。现在我们再来看一下 identity 函数:

  1. function identity <T, U>(value: T, message: U) : T { 
  2.   console.log(message); 
  3.   return value; 

相比之前定义的 identity 函数,新的 identity 函数增加了一个类型变量 U,但该函数的返回类型我们仍然使用 T。如果我们想要返回两种类型的对象该怎么办呢?针对这个问题,我们有多种方案,其中一种就是使用 TypeScript 中的元组:

  1. function identity <T, U>(value: T, message: U) : [T, U] { 
  2.   return [value, message]; 

二、泛型接口

要解决函数中返回多种类型对象的问题,我们可以创建一个用于的 identity 函数通用 Identities 接口:

  1. interface Identities<V, M> { 
  2.   value: V, 
  3.   message: M 

在上述的 Identities 接口中,我们引入了类型变量 V 和 M,来进一步说明有效的字母都可以用于表示类型变量,之后我们就可以将 Identities 接口作为 identity 函数的返回类型:

  1. function identity<T, U> (value: T, message: U): Identities<T, U> { 
  2.   console.log(value + ": " + typeof (value)); 
  3.   console.log(message + ": " + typeof (message)); 
  4.   let identities: Identities<T, U> = { 
  5.     value, 
  6.     message 
  7.   }; 
  8.   return identities; 
  9.  
  10. console.log(identity(68, "Semlinker")); 

以上代码成功运行后,在控制台会输出以下结果:

  1. 68: number 
  2. Semlinker: string 
  3. {value: 68, message: "Semlinker"

泛型除了可以应用在函数和接口之外,它也可以应用在类中,下面我们就来看一下在类中如何使用泛型。

三、泛型类

在类中使用泛型也很简单,我们只需要在类名后面,使用 的语法定义任意多个类型变量,具体示例如下:

  1. interface GenericInterface<U> { 
  2.   value: U 
  3.   getIdentity: () => U 
  4.  
  5. class IdentityClass<T> implements GenericInterface<T> { 
  6.   value: T 
  7.  
  8.   constructor(value: T) { 
  9.     this.value = value 
  10.   } 
  11.  
  12.   getIdentity(): T { 
  13.     return this.value 
  14.   } 
  15.  
  16. const myNumberClass = new IdentityClass<number>(68); 
  17. console.log(myNumberClass.getIdentity()); // 68 
  18.  
  19. const myStringClass = new IdentityClass<string>("Semlinker!"); 
  20. console.log(myStringClass.getIdentity()); // Semlinker! 

接下来我们以实例化 myNumberClass 为例,来分析一下其调用过程:

  • 在实例化 IdentityClass 对象时,我们传入 number 类型和构造函数参数值 68;
  • 之后在 IdentityClass 类中,类型变量 T 的值变成 number 类型;
  • IdentityClass 类实现了 GenericInterface,而此时 T 表示 number 类型,因此等价于该类实现了 GenericInterface 接口;
  • 而对于 GenericInterface 接口来说,类型变量 U 也变成了 number。这里我有意使用不同的变量名,以表明类型值沿链向上传播,且与变量名无关。

相信看到这里一些读者会有疑问,我们什么时候需要使用泛型呢?通常在决定是否使用泛型时,我们有以下两个参考标准:

  • 当你的函数、接口或类将处理多种数据类型时;
  • 当函数、接口或类在多个地方使用该数据类型时。

很有可能你没有办法保证在项目早期就使用泛型的组件,但是随着项目的发展,组件的功能通常会被扩展。这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下,引入泛型将比复制组件来满足一系列数据类型更方便。

我们将在本文的后面探讨更多满足这两个条件的用例。不过在这样做之前,让我们先介绍一下 Typescript 泛型提供的其他功能。

四、泛型约束

有时我们可能希望限制每个类型变量接受的类型数量,这就是泛型约束的作用。下面我们来举几个例子,介绍一下如何使用泛型约束。

4.1 确保属性存在

有时候,我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。

一个很好的例子是在处理字符串或数组时,我们会假设 length 属性是可用的。让我们再次使用 identity 函数并尝试输出参数的长度:

  1. function identity<T>(arg: T): T { 
  2.   // Property 'length' does not exist on type 'T'.(2339) 
  3.   console.log(arg.length); // Error 
  4.   return arg; 

在这种情况下,编译器没法确认 T 类型一定含有 length 属性,尤其是在可以将任何类型赋给类型变量 T 的情况下。我们需要做的就是让类型变量 extends 一个含有我们所需属性的接口,比如这样:

  1. interface Length { 
  2.   length: number; 
  3.  
  4. function identity<T extends Length>(arg: T): T { 
  5.   console.log(arg.length); // 可以获取length属性 
  6.   return arg; 

T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。之后,当我们使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 会提示相关的错误信息:

  1. identity(68); // Error 
  2. // Argument of type '68' is not assignable to parameter of type 'Length'.(2345) 

此外,我们还可以使用 , 号来分隔多种约束类型,比如:。而对于上述的 length 属性问题来说,如果我们显式地将变量设置为数组类型,也可以解决该问题,具体方式如下:

  1. function identity<T>(arg: T[]): T[] { 
  2.   console.log(arg.length);   
  3.   return arg;  

4.2 检查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。 下面我们来举个 keyof 的使用示例:

  1. interface Person { 
  2.   name: string; 
  3.   age: number; 
  4.   location: string; 
  5.  
  6. type K1 = keyof Person; // "name" | "age" | "location" 
  7. type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ... 
  8. type K3 = keyof { [x: string]: Person };  // string | number 

提示:TypeScript Playground v4.2.3 版本以上的编译器不会显示 keyof 操作符的结果

通过 keyof 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。具体的使用方式如下:

  1. function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 
  2.   return obj[key]; 

在以上的 getProperty 函数中,我们通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会出现运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key]; 是不同的。

下面我们来看一下如何使用 getProperty 函数:

  1. enum Difficulty { 
  2.   Easy, 
  3.   Intermediate, 
  4.   Hard 
  5.  
  6. function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 
  7.   return obj[key]; 
  8.  
  9. let tsInfo = { 
  10.    name"Typescript"
  11.    supersetOf: "Javascript"
  12.    difficulty: Difficulty.Intermediate 
  13.   
  14. let difficulty: Difficulty =  
  15.   getProperty(tsInfo, 'difficulty'); // OK 
  16.  
  17. let supersetOf: string =  
  18.   getProperty(tsInfo, 'superset_of'); // Error 

在以上示例中,对于 getProperty(tsInfo, 'superset_of') 这个表达式,TypeScript 编译器会提示以下错误信息:

  1. Argument of type '"superset_of"' is not assignable to parameter of type '"difficulty" | "name" | "supersetOf"'

很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。接下来,我们来介绍一下泛型参数默认类型。

五、泛型参数默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。

泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 ,对应的使用示例如下:

  1. interface Person<T=string> { 
  2.   id: T; 
  3.  
  4. const p0: Person = { id: "lolo" }; 
  5. const p1: Person<number> = { id: 28 }; 

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的。
  • 必选的类型参数不能在可选的类型参数后。
  • 如果类型参数有约束,类型参数的默认类型必须满足这个约束。
  • 当指定类型实参时,你只需要指定必选类型参数的类型实参。未指定的类型参数会被解析为它们的默认类型。
  • 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。
  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。
  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型。

六、泛型条件类型

在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

  1. T extends U ? X : Y 

以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取:

  1. interface Dictionary<T = any> { 
  2.   [key: string]: T; 
  3.   
  4. type StrDict = Dictionary<string> 
  5.  
  6. type DictMember<T> = T extends Dictionary<infer V> ? V : never 
  7. type StrDictMember = DictMember<StrDict> // string 

在上面示例中,当类型 T 满足 T extends Dictionary 约束时,我们会使用 infer 关键字声明了一个类型变量 V,并返回该类型,否则返回 never 类型。

在 TypeScript 中,never 类型表示的是那些永不存在的值的类型。例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

另外,需要注意的是,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。即使 any 也不可以赋值给 never。

条件类型还有一个特性:分布式条件类型。当检测的类型是由 ”裸类型“(指该类型未被包装过) 组成的联合类型时,条件类型会被自动分发成联合类型。以 T extends U ? X : Y 条件类型为例,当类型参数的为 A | B | C 时,该条件类型将会被解析为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。

分布式条件类型的使用示例如下:

  1. type TypeName<T> = T extends string 
  2.   ? "string" 
  3.   : T extends number 
  4.   ? "number" 
  5.   : T extends boolean 
  6.   ? "boolean" 
  7.   : T extends undefined 
  8.   ? "undefined" 
  9.   : T extends Function 
  10.   ? "function" 
  11.   : "object"
  12.  
  13. type T10 = TypeName<string | (() => void)>; // "string" | "function" 
  14. type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined" 
  15. type T11 = TypeName<string[] | number[]>; // "object" 

七、泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍其中几个常用的工具类型。

7.1 Partial

Partial 的作用就是将某个类型里的属性全部变为可选项 ?。

定义:

  1. type Partial<T> = { 
  2.   [P in keyof T]?: T[P]; 
  3. }; 

以上 Partial 类型被称为映射类型,用于把已有的类型转换成新的类型。在以上代码中,我们首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给类型变量 P,最后通过 T[P] 取得属性 P 对应的类型。中间的 ? 号,表示将属性变为可选。

示例:

  1. interface Todo { 
  2.   title: string; 
  3.   description: string; 
  4.  
  5. function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) { 
  6.   return { ...todo, ...fieldsToUpdate }; 
  7.  
  8. const todo1 = { 
  9.   title: "Learn TS"
  10.   description: "Learn TypeScript" 
  11. }; 
  12.  
  13. const todo2 = updateTodo(todo1, { 
  14.   description: "Learn TypeScript Handbook" 
  15. }); 

在上面的 updateTodo 方法中,我们利用 Partial 工具类型,定义 fieldsToUpdate 的类型为 Partial,即:

  1.    title?: string | undefined; 
  2.    description?: string | undefined; 

是不是觉得 Partial 使用起来挺简单的,那么如何定义一个 SetOptional 工具类型,支持把给定的 keys 对应的属性变成可选的。对应的使用示例如下所示:

  1. type Foo = { 
  2.  a: number; 
  3.  b?: string; 
  4.  c: boolean; 
  5.  
  6. // 测试用例 
  7. type SomeOptional = SetOptional<Foo, 'a' | 'b'>; 
  8.  
  9. // type SomeOptional = { 
  10. //  a?: number; // 该属性已变成可选的 
  11. //  b?: string; // 保持不变 
  12. //  c: boolean;  
  13. // } 

7.2 Record

Record 的作用是将 K 中所有的属性的值转化为 T 类型。

定义:

  1. type Record<K extends keyof any, T> = { 
  2.   [P in K]: T; 
  3. }; 

示例:

  1. interface PageInfo { 
  2.   title: string; 
  3.  
  4. type Page = "home" | "about" | "contact"
  5.  
  6. const x: Record<Page, PageInfo> = { 
  7.   about: { title: "about" }, 
  8.   contact: { title: "contact" }, 
  9.   home: { title: "home" } 
  10. }; 

7.3 Pick

Pick 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

定义:

  1. type Pick<T, K extends keyof T> = { 
  2.   [P in K]: T[P]; 
  3. }; 

示例:

  1. interface Todo { 
  2.   title: string; 
  3.   description: string; 
  4.   completed: boolean; 
  5.  
  6. type TodoPreview = Pick<Todo, "title" | "completed">; 
  7.  
  8. const todo: TodoPreview = { 
  9.   title: "Learn TS"
  10.   completed: false 
  11. }; 

在掌握 Pick 的用法之后,你可以想一下,如何定义一个 ConditionalPick 工具类型,支持根据指定的 Condition 条件来生成新的类型,对应的使用示例如下:

  1. interface Example { 
  2.  a: string; 
  3.  b: string | number; 
  4.  c: () => void; 
  5.  d: {}; 
  6.  
  7. // 测试用例: 
  8. type StringKeysOnly = ConditionalPick<Example, string>; 
  9. //=> { a: string } 

7.4 Exclude

Exclude 的作用是将某个类型中属于另一个的类型移除掉。

定义:

  1. type Exclude<T, U> = T extends U ? never : T; 

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

示例:

  1. type T0 = Exclude<"a" | "b" | "c""a">; // "b" | "c" 
  2. type T1 = Exclude<"a" | "b" | "c""a" | "b">; // "c" 
  3. type T2 = Exclude<string | number | (() => void), Function>; // string | number 

由以上结果可知,Exclude 工具类型利用了前面介绍的分布式条件类型的特性。

7.5 ReturnType

ReturnType 的作用是用于获取函数 T 的返回类型。

定义:

  1. type ReturnType any> = T extends (...args: any) => infer R ? R : any

示例:

  1. type T0 = ReturnType<() => string>; // string 
  2. type T1 = ReturnType<(s: string) => void>; // void 
  3. type T2 = ReturnType<<T>() => T>; // {} 
  4. type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[] 
  5. type T4 = ReturnType<any>; // any 
  6. type T5 = ReturnType<never>; // any 
  7. type T6 = ReturnType<string>; // Error 
  8. type T7 = ReturnType<Function>; // Error 

简单介绍了泛型工具类型,最后我们来介绍如何使用泛型来创建对象。

八、使用泛型创建对象

8.1 构造签名

有时,泛型类可能需要基于传入的泛型 T 来创建其类型相关的对象。比如:

  1. class FirstClass { 
  2.   id: number | undefined; 
  3.  
  4. class SecondClass { 
  5.   name: string | undefined; 
  6.  
  7. class GenericCreator<T> { 
  8.   create(): T { 
  9.     return new T(); 
  10.   } 
  11.  
  12. const creator1 = new GenericCreator<FirstClass>(); 
  13. const firstClass: FirstClass = creator1.create(); 
  14.  
  15. const creator2 = new GenericCreator<SecondClass>(); 
  16. const secondClass: SecondClass = creator2.create(); 

在以上代码中,我们定义了两个普通类和一个泛型类 GenericCreator。在通用的 GenericCreator 泛型类中,我们定义了一个名为 create 的成员方法,该方法会使用 new 关键字来调用传入的实际类型的构造函数,来创建对应的对象。但可惜的是,以上代码并不能正常运行,对于以上代码,在 TypeScript v4.4.3 编译器下会提示以下错误:

  1. 'T' only refers to a type, but is being used as a value here. 

这个错误的意思是:T 类型仅指类型,但此处被用作值。那么如何解决这个问题呢?根据 TypeScript 文档,为了使通用类能够创建 T 类型的对象,我们需要通过其构造函数来引用 T 类型。对于上述问题,在介绍具体的解决方案前,我们先来介绍一下构造签名。

在 TypeScript 接口中,你可以使用 new 关键字来描述一个构造函数:

  1. interface Point { 
  2.   new (x: number, y: number): Point; 

以上接口中的 new (x: number, y: number) 我们称之为构造签名,其语法如下:

ConstructSignature:new?TypeParametersopt?(?ParameterListopt?)?TypeAnnotationopt

在上述的构造签名中,TypeParametersopt 、ParameterListopt 和 TypeAnnotationopt 分别表示:可选的类型参数、可选的参数列表和可选的类型注解。与该语法相对应的几种常见的使用形式如下:

  1. new C   
  2. new C ( ... )   
  3. new C < ... > ( ... ) 

介绍完构造签名,我们再来介绍一个与之相关的概念,即构造函数类型。

8.2 构造函数类型

在 TypeScript 语言规范中这样定义构造函数类型:

An object type containing one or more construct signatures is said to be a constructor type. Constructor types may be written using constructor type literals or by including construct signatures in object type literals.

通过规范中的描述信息,我们可以得出以下结论:

  • 包含一个或多个构造签名的对象类型被称为构造函数类型;
  • 构造函数类型可以使用构造函数类型字面量或包含构造签名的对象类型字面量来编写。

那么什么是构造函数类型字面量呢?构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来说,构造函数类型字面量的形式如下:

  1. new < T1, T2, ... > ( p1, p2, ... ) => R 

该形式与以下对象类型字面量是等价的:

  1. { new < T1, T2, ... > ( p1, p2, ... ) : R } 

下面我们来举个实际的示例:

  1. // 构造函数类型字面量 
  2.  
  3. new (x: number, y: number) => Point 

等价于以下对象类型字面量:

  1.    new (x: number, y: number): Point; 

8.3 构造函数类型的应用

在介绍构造函数类型的应用前,我们先来看个例子:

  1. interface Point { 
  2.   new (x: number, y: number): Point; 
  3.   x: number; 
  4.   y: number; 
  5.  
  6. class Point2D implements Point { 
  7.   readonly x: number; 
  8.   readonly y: number; 
  9.  
  10.   constructor(x: number, y: number) { 
  11.     this.x = x; 
  12.     this.y = y; 
  13.   } 
  14.  
  15. const point: Point = new Point2D(1, 2); 

对于以上的代码,TypeScript 编译器会提示以下错误信息:

  1. Class 'Point2D' incorrectly implements interface 'Point'
  2. Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'

相信很多刚接触 TypeScript 不久的小伙伴都会遇到上述的问题。要解决这个问题,我们就需要把对前面定义的 Point 接口进行分离,即把接口的属性和构造函数类型进行分离:

  1. interface Point { 
  2.   x: number; 
  3.   y: number; 
  4.  
  5. interface PointConstructor { 
  6.   new (x: number, y: number): Point; 

完成接口拆分之后,除了前面已经定义的 Point2D 类之外,我们又定义了一个 newPoint 工厂函数,该函数用于根据传入的 PointConstructor 类型的构造函数,来创建对应的 Point 对象。

  1. class Point2D implements Point { 
  2.   readonly x: number; 
  3.   readonly y: number; 
  4.  
  5.   constructor(x: number, y: number) { 
  6.     this.x = x; 
  7.     this.y = y; 
  8.   } 
  9.  
  10. function newPoint( 
  11.   pointConstructor: PointConstructor, 
  12.   x: number, 
  13.   y: number 
  14. ): Point { 
  15.   return new pointConstructor(x, y); 
  16.  
  17. const point: Point = newPoint(Point2D, 1, 2); 

8.4 使用泛型创建对象

了解完构造签名和构造函数类型之后,下面我们来开始解决上面遇到的问题,首先我们需要重构一下 create 方法,具体如下所示:

  1. class GenericCreator<T> { 
  2.   create<T>(c: { new (): T }): T { 
  3.     return new c(); 
  4.   } 

在以上代码中,我们重新定义了 create 成员方法,根据该方法的签名,我们可以知道该方法接收一个参数,其类型是构造函数类型,且该构造函数不包含任何参数,调用该构造函数后,会返回类型 T 的实例。

如果构造函数含有参数的话,比如包含一个 number 类型的参数时,我们可以这样定义 create 方法:

  1. create<T>(c: { new(a: number): T; }, num: number): T { 
  2.   return new c(num); 

更新完 GenericCreator 泛型类,我们就可以使用下面的方式来创建 FirstClass 和 SecondClass 类的实例:

  1. const creator1 = new GenericCreator<FirstClass>(); 
  2. const firstClass: FirstClass = creator1.create(FirstClass); 
  3.  
  4. const creator2 = new GenericCreator<SecondClass>(); 
  5. const secondClass: SecondClass = creator2.create(SecondClass); 

8.5 抽象构造签名

在 TypeScript 4.2 版本中引入了抽象构造签名,用于解决以下的问题:

  1. type ConstructorFunction = new (...args: any[]) => any
  2.  
  3. abstract class Utilities {} 
  4.  
  5. // Type 'typeof Utilities' is not assignable to type 'ConstructorFunction'
  6. // Cannot assign an abstract constructor type to a non-abstract constructor type. 
  7. let UtilityClass: ConstructorFunction = Utilities; // Error. 

由以上的错误信息可知,我们不能把抽象构造器类型分配给非抽象的构造器类型。针对这个问题,我们需要使用 abstract 修饰符:

  1. declare type ConstructorFunction = abstract new (...args: any[]) => any

需要注意的是,对于抽象构造器类型,我们也可以传入具体的实现类:

  1. declare type ConstructorFunction = abstract new (...args: any[]) => any
  2.  
  3. abstract class Utilities {} 
  4. class UtilitiesConcrete extends Utilities {} 
  5.  
  6. let UtilityClass: ConstructorFunction = Utilities; // Ok 
  7. let UtilityClass1: ConstructorFunction = UtilitiesConcrete; // Ok 

而对于 TypeScript 4.2 以下的版本,我们可以通过以下方式来解决上面的问题:

  1. type Constructor<T> = Function & { prototype: T } 
  2.  
  3. abstract class Utilities {} 
  4.  
  5. class UtilitiesConcrete extends Utilities {} 
  6.  
  7. let UtilityClass: Constructor<Utilities> = Utilities; 
  8. let UtilityClass1: Constructor<UtilitiesConcrete> = UtilitiesConcrete; 

九、可变元组类型

在 TypeScript 4.0 版本支持可变元组类型,其中有两个新的变化。第一个变化是元组类型的展开运算可以支持泛型了:

  1. function tail<T extends any[]>(arr: readonly [any, ...T]) { 
  2.   const [_ignored, ...rest] = arr; 
  3.   return rest; 
  4.  
  5. const myTuple = [1, 2, 3, 4] as const; 
  6. const myArray = ["hello""world"]; 
  7.   
  8. const r1 = tail(myTuple); // r1: [2, 3, 4] 
  9. const r2 = tail([...myTuple, ...myArray] as const); // r2: [2, 3, 4, ...string[]] 

第二个变化是 rest 元素可以出现在元组中的任何位置,而不仅仅是在结尾!

  1. type Strings = [string, string]; 
  2. type Numbers = [number, number]; 
  3.  
  4. type StrStrNumNumBool = [...Strings, ...Numbers, boolean]; 

对于 TypeScript 4.0 以下的版本,以上代码将会出现以下的错误信息:

  1. A rest element must be last in a tuple type.(1256) 

利用这两个特性,我们就可以实现一个类型良好的 concat 函数:

  1. type Arr = readonly any[]; 
  2.   
  3. function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] { 
  4.   return [...arr1, ...arr2]; 
  5.  
  6. const arr3 = concat([1, 2, 3], ["a""b""c"]) 

关于可变元组类型的相关内容,就不展开介绍了,感兴趣的小伙伴可以自行阅读 TypeScript 4.0 的相关文档。

十、泛型是如何工作的

最后,阿宝哥将使用 ts-ast-viewer 在线工具,带大家换个角度来学习 TypeScript 的泛型。对应的示例代码如下:

  1. type Head<T extends Array<any>> = T extends [any, ...any] ? T[0] : never 
  2.  
  3. type H0 = Head<[1, 2, 3]> // 1 

10.1 类型变量 AST

你不知道的 TypeScript 泛型

10.2 条件类型 AST

你不知道的 TypeScript 泛型

10.3 类型引用 AST

你不知道的 TypeScript 泛型

所使用在线工具的地址为:https://ts-ast-viewer.com/

建议大家实际使用一下 ts-ast-viewer 这个在线工具,详细看一下生成的节点,这样的话,可以让你更好地理解 TypeScript 的泛型。

原文链接:https://mp.weixin.qq.com/s/jF0ZEGM9BRQgyKPZ9W9MpA