深入 TypeScript 当中的泛型

深入 TypeScript 当中的泛型

我们在之前的 重温 TypeScript深入 TypeScript 的章节当中介绍了一些 TypeScript 的基本使用方式和一些进阶内容,本来是打算将泛型相关内容一同整理到其中的,但是梳理下来发现泛型涉及到的内容还是比较多的,所以就另起篇幅来单独介绍介绍泛型的相关内容,主要参考的是 Typescript Generics,在原文基础之上有所调整,主要是方便自己理解,想要了解更为详细的内容可以参考原文

下面就让我们从头开始看起,其中主要包括以下相关内容

  • 泛型是什么
  • 泛型接口
  • 泛型类
  • 泛型约束
  • 泛型参数默认类型
  • 泛型条件类型
  • 泛型工具类型
  • 使用泛型创建对象

泛型是什么

其实简单来说,设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是类的实例成员、类的方法、函数参数和函数返回值等

也就是说,泛型允许我们同一个函数接受不同类型参数,相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型,下面我们来一步一步揭示泛型的作用,就从一个通用的 identity 函数开始看起,该函数接收一个参数并直接返回它

1
2
3
4
5
function identity(value) {
return value
}

identity(1)

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

1
2
3
4
5
6
7
function identity(value: Number): Number {
return value
}

identity(1) // ✅

identity('1') // ❌

这里 identity 的问题是我们将 Number 类型分配给参数和返回类型,使该函数仅可用于该原始类型,此时如果我们传入一个字符串类型的参数,编辑器会提示我们有错误存在,也就是说该函数并不是可扩展或通用的,很明显这并不是我们所希望的

我们确实可以把 Number 换成联合类型又或是 any,但是我们失去了定义应该返回哪种类型的能力,并且在这个过程中使编译器失去了类型保护的作用,我们的目标是让 identity 函数可以适用于任何特定的类型,为了实现这个目标,我们可以使用泛型来解决这个问题,具体实现方式如下

1
2
3
4
5
6
7
function identity<T>(value: T): T {
return value
}

identity<Number>(1) // ✅

identity<string>('1') // ✅

看上去是不是很轻松,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型,如下图所示

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

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

  • KKey),表示对象中的键类型
  • VValue),表示对象中的值类型
  • EElement),表示元素类型

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

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

identity<Number, string>(18, 'zhangsan')

这里我们在使用的时候明确的指定了 TUNumberstring 类型,并做为一个参数传给函数,使用了 <> 括起来而不是 (),但是另外一种更为普遍的做法是使用『类型推论』,我们可以完全省略尖括号,也就是说让编译器来根据我们传入的参数自动地来确定 T 或者 U 的类型,从而使代码更简洁

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

identity(18, 'zhangsan')

这一点我们可以也通过编辑器的代码提示功能来进行发现

1
2
3
4
5
6
7
8
// function identity<18, number>(value: 18, message: number): 18
identity(18, 18)

// function identity<'abc', number>(value: 'abc', message: number): 'abc'
identity('abc', 18)

// function identity<'abc', string>(value: 'abc', message: string): 'abc'
identity('abc', 'abc')

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

1
2
3
4
function identity<T = string, U = number>(value: T, message: U): T {
console.log(message)
return value
}

以上过程,我们可以参考下面这张动图,来直观地感受一下类型传递的过程

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

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

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

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

虽然使用元组可以解决上述的问题,但是有没有其它更好的解决方案呢?答案是有的,那就是我们可以使用『泛型接口』

泛型接口

为了解决上面提到的问题,首先让我们创建一个用于的 identity 函数通用 Identities 接口

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

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

1
2
3
4
5
6
7
8
9
function identity<T, U>(value: T, message: U): Identities<T, U> {
let identities: Identities<T, U> = {
value,
message
}
return identities
}

identity(18, 'zhangsan')

运行后可以发现,是可以正常运行的,当然泛型除了可以应用在函数和接口之外,它也可以应用在类中,下面我们就来看一下在类中如何使用泛型

泛型类

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface GenericInterface<U> {
value: U
getIdentity: () => U
}

class IdentityClass<T> implements GenericInterface<T> {
value: T
constructor(value: T) {
this.value = value
}
getIdentity(): T {
return this.value
}

}

const myNumberClass = new IdentityClass<Number>(18)
myNumberClass.getIdentity() // 18

const myStringClass = new IdentityClass<string>('zhangsan')
myStringClass.getIdentity() // zhangsan

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

  • 在实例化 IdentityClass 对象时,我们传入 Number 类型和构造函数参数值 18,之后在 IdentityClass 类中,类型变量 T 的值变成 Number 类型
  • IdentityClass 类实现了 GenericInterface<T>,而此时 T 表示 Number 类型,因此等价于该类实现了 GenericInterface<Number> 接口
  • 而对于 GenericInterface<U> 接口来说,类型变量 U 也变成了 Number

所以说,使用泛型类可确保在整个类中一致地使用指定的数据类型,通常在决定是否使用泛型时,我们有以下两个参考标准

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

通常而言,但是随着项目的发展,组件的功能通常会被扩展,这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下引入泛型将比复制组件来满足一系列数据类型更干净,下面我们再来看看 Typescript 泛型提供的一些其他功能

泛型约束

有时我们可能希望限制每个类型变量接受的类型数量,而这就是泛型约束的作用,下面我们通过几个例子来了解一下泛型约束

确保属性存在

有时候,我们希望类型变量对应的类型上存在某些属性,在这种情况下,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在,一个很好的例子是在处理字符串或数组时,我们会假设 length 属性是可用的,还是以上面的示例为例,我们来简单的调整一下,尝试输出参数的长度

1
2
3
4
5
function identity<T>(arg: T): T {
// ❌
console.log(arg.length)
return arg
}

在上面的示例当中,我们想访问 arglength 属性,但是编译器并不能证明每种类型都有 length 属性,所以就报错了,在这种情况下,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量,这也被称为『泛型约束』

在这种情况下,编译器将不会知道 T 确实含有 length 属性,尤其是在可以将任何类型赋给类型变量 T 的情况下,所以我们需要做的就是让类型变量 extends 一个含有我们所需属性的接口

1
2
3
4
5
6
7
8
9
10
11
12
interface Length {
length: number
}

function identity<T extends Length>(arg: T): T {
console.log(arg.length)
return arg
}

identity('zhangsan') // ✅

identity(1234567890) // ❌

<T extends Length> 用于告诉编译器,我们支持已经实现 Length 接口的任何类型,之后当我们使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 都会提示我们相关的错误信息,也就是说,现在这个泛型函数已经被定义了约束,因此它不再是适用于任意类型,所以我们需要传入符合约束类型的值,当然具有 length 属性的对象也是可以的

1
2
// ✅
identity({ length: 10, value: 3 })

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

1
2
3
4
5
6
7
8
9
10
11
function identity<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}

// or

function identity<T>(arg: Array<T>): Array<T> {
console.log(arg.length)
return arg
}

另外,多个类型参数之间也是可以互相约束的,比如下面这个示例

1
2
3
4
5
6
7
8
9
10
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id]
}
return target
}

let x = { a: 1, b: 2, c: 3, d: 4 }

copyFields(x, { b: 10, d: 20 })

在上面的示例当中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段,其中 <T>source 的写法等同于 source as T,其实就是把 source 断言成 T 类型

检查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在,而这一点主要依靠的是 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

1
2
3
4
5
6
7
8
interface Person {
name: string
age: number
}

type K2 = keyof Person[] // number | 'length' | 'push' ...
type K1 = keyof Person // 'name' | 'age'
type K3 = keyof { [x: string]: Person } // string | number

这里关于 keyof { [x: string]: Person } 的使用方式我们多提及一些,我们在之前的章节当中曾经提到过,在 TypeScript 中支持两种索引签名,数字索引和字符串索引

1
2
3
4
5
6
7
8
9
interface StringArray {
// 字符串索引 => keyof StringArray => string | number
[index: string]: string;
}

interface StringArray1 {
// 数字索引 => keyof StringArray1 => number
[index: number]: string;
}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类,其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引,所以 keyof { [x: string]: Person } 的结果会返回 string | number

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Difficulty {
Easy,
Intermediate,
Hard
}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}

let tsInfo = {
name: 'Typescript',
supersetOf: 'Javascript',
difficulty: Difficulty.Intermediate
}

let difficulty: Difficulty = getProperty(tsInfo, 'difficulty') // ✅

let supersetOf: string = getProperty(tsInfo, 'superset_of') // ❌

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

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

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

泛型参数默认类型

这个特性我们在上面也简单提及过,在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型,当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用,泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 <T = Default Type>,对应的使用示例如下

1
2
3
4
5
6
interface A<T = string> {
name: T
}

const str: A = { name: 'zhangsan' }
const num: A<number> = { name: 18 }

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

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

泛型条件类型

TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束,尽管代码中使用了 extends 关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性,条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一

1
T extends U ? X : Y

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

1
2
3
4
5
6
7
8
9
interface Dictionary<T = any> {
[key: string]: T
}

type StrDict = Dictionary<string>

type DictMember<T> = T extends Dictionary<infer V> ? V : never

type StrDictMember = DictMember<StrDict> // string

在上面示例中,当类型 T 满足 T extends Dictionary 约束时,我们会使用 infer 关键字声明了一个类型变量 V(关于 infer 我们下面会进行介绍),并返回该类型,否则返回 never 类型,这里关于 never 类型,我们多提及一点

TypeScript 中,never 类型表示的是那些永不存在的值的类型,例如 never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型,另外,需要注意的是,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外),即使 any 也不可以赋值给 never

除了上述的应用外,利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function stringPromise() {
return 'hello, zhangsan'
}

interface Person {
name: string
age: number
}

async function personPromise() {
return { name: 'zhangsan', age: 30 } as Person
}

type PromiseType<T> = (args: any[]) => Promise<T>
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never

type extractStringPromise = UnPromisify<typeof stringPromise> // string
type extractPersonPromise = UnPromisify<typeof personPromise> // Person

泛型工具类型

为了方便开发者,TypeScript 内置了一些常用的工具类型,比如 PartialRequiredReadonlyRecordReturnType 等,不过在详细展开之前我们先来了解一下几个比较常用的操作符

  • in 操作符

in 操作符可以用来遍历枚举类型

1
2
3
4
5
6
type Keys = 'a' | 'b' | 'c'

// => { a: any, b: any, c: any }
type Obj = {
[p in Keys]: any
}
  • typeof 操作符

typeof 操作符可以用来获取一个变量声明或对象的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
name: string
age: number
}

const sem: Person = { name: 'zhangsan', age: 18 }

function toArray(x: number): Array<number> {
return [x]
}

type Sem = typeof sem // => Person
type Func = typeof toArray // => (x: number) => Array<number>

需要注意以下这种使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Greeter {
static message = 'hello'
greet() {
return Greeter.message
}
}

// 获取的是实例的类型,该类型可以获取实例对象上的属性或方法
let greeter1: Greeter = new Greeter()
greeter1.greet() // 'hello'


// 获取的是类的类型,该类型可以获取类上面的静态属性或方法
let greeterTwo: typeof Greeter = Greeter
greeterTwo.message = 'hey'

let greeter2: Greeter = new greeterTwo()
greeter2.greet() // 'hey'
  • infer 操作符

infer 最早是出现在这个 PR 当中,表示在 extends 条件语句中待推断的类型变量

1
type ParamType<T> = T extends (param: infer P) => any ? P : T

在这个条件语句 T extends (param: infer P) => any ? P : T 中,infer P 表示待推断的函数参数,如果 T 能赋值给 (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,否则返回为 T

1
2
3
4
5
6
7
8
9
10
11
type ParamType<T> = T extends (param: infer P) => any ? P : T

interface User {
name: string
age: number
}

type Func = (user: User) => void

type Param = ParamType<Func> // Param = User
type AA = ParamType<string> // string

而在 2.8 版本中,TypeScript 也已经内置了一些与 infer 有关的映射类型,比如用于提取函数类型的返回值类型

1
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any

相比于之前的示例,ReturnType<T> 只是将 infer P 从参数位置移动到返回值位置,因此此时 P 即是表示待推断的返回值类型

1
2
type Func = () => User
type Test = ReturnType<Func> // Test = User

另外还可以用于提取构造函数中参数(实例)类型,比如一个构造函数可以使用 new 来实例化,因此它的类型通常表示如下

1
type Constructor = new (...args: any[]) => any

infer 用于构造函数类型中,可用于参数位置 new (...args: infer P) => any 和返回值位置 new (...args: any[]) => infer P,因此就内置如下两个映射类型

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取参数类型
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never

// 获取实例类型
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any

class TestClass {
constructor(public name: string, public age: number) { }
}

type Params = ConstructorParameters<typeof TestClass> // [string, number]

type Instance = InstanceType<typeof TestClass> // TestClass

当然关于 infer 还有许多的『骚操作』,比如 tupleunionuniontuple 等等,这里就不详细展开了,可以参考 infer 的一些用例union to tuple 这两个链接来了解更多

  • extends 操作符

这个我们之前提到过,有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number
}

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

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型

1
2
// => 类型 `number` 的参数不能赋给类型 `Lengthwise` 的参数
identity(3)

这时我们需要传入符合约束类型的值,必须包含必须的属性

1
2
3
4
5
// ❌
identity({ value: 3 })

// ✅
identity({ length: 10, value: 3 })
  • Omit 操作符

有时候我们需要复用一个类型,但是又不需要此类型内的全部属性,因此需要剔除某些属性,这个方法在 React 中经常用到,当父组件通过 props 向下传递数据的时候,通常需要复用父组件的 props 类型,但是又需要剔除一些无用的类型

1
2
3
4
5
6
7
8
9
interface User {
username: string
id: number
token: string
avatar: string
role: string
}

type UserWithoutToken = Omit<User, 'token'>

下面我们就正式来看看之前提到的 TypeScript 当中内置的一些常用的工具类型

Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?,定义如下

1
2
3
4
5
6
7
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P]
}

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值,中间的 ? 号,用于将所有属性变为可选,比如下面这个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Todo {
title: string
description: string
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate }
}

const todo1 = {
title: 'title',
description: 'description',
}

const todo2 = updateTodo(todo1, {
description: 'update-description',
})

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

1
2
3
4
{
title?: string | undefined
description?: string | undefined
}

Record

Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型,定义如下

1
2
3
4
5
6
7
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T
}

一个示例

1
2
3
4
5
6
7
8
9
10
11
interface PageInfo {
title: string
}

type Page = 'home' | 'about' | 'contact'

const x: Record<Page, PageInfo> = {
about: { title: 'about' },
contact: { title: 'contact' },
home: { title: 'home' }
}

Pick

Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型

1
2
3
4
5
6
7
/**
* node_modules/typescript/lib/lib.es5.d.ts
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}

一个示例

1
2
3
4
5
6
7
8
9
10
11
12
interface Todo {
title: string
description: string
completed: boolean
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
title: 'home',
completed: false
}

另外一个示例

1
2
3
4
5
6
interface Test {
arr: string[]
}

// 返回的结果 => {arr: string[]}
let aaa: Pick<Test, 'arr'> = { arr: ['1'] }

Exclude

Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉,定义如下

1
2
3
4
5
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T

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

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

ReturnType

ReturnType<T> 的作用是用于获取函数 T 的返回类型,定义如下

1
2
3
4
5
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

几个简单的示例

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

在了解完了泛型工具类型以后,最后我们再来看看如何使用泛型来创建对象

使用泛型创建对象

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FirstClass {
id: number | undefined
}

class SecondClass {
name: string | undefined
}

class GenericCreator<T> {
create(): T {
// ❌
return new T()
}
}

const creator1 = new GenericCreator<FirstClass>()
const firstClass: FirstClass = creator1.create()

const creator2 = new GenericCreator<SecondClass>()
const secondClass: SecondClass = creator2.create()

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

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

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

构造签名

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

1
2
3
interface Point {
new(x: number, y: number): Point
}

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

1
ConstructSignature: new TypeParametersopt(ParameterListopt) TypeAnnotationopt

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

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

在了解完构造签名以后,我们再来看看一个与之相关的概念,即『构造函数类型』

构造函数类型

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

1
2
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
// 构造函数类型字面量
new (x: number, y: number) => Point

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

1
2
3
{
new (x: number, y: number): Point
}

构造函数类型的应用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Point {
new(x: number, y: number): Point
x: number
y: number
}

class Point2D implements Point {
readonly x: number
readonly y: number

constructor(x: number, y: number) {
this.x = x
this.y = y
}
}

const point: Point = new Point2D(1, 2)

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

1
2
3
Class 'Point2D' incorrectly implements interface 'Point'.

Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.

我们先来尝试着解决这个错误,要解决这个问题,我们就需要把对前面定义的 Point 接口进行分离,即把接口的属性和构造函数类型进行分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface Point {
x: number
y: number
}

interface PointConstructor {
new(x: number, y: number): Point
}

class Point2D implements Point {
readonly x: number
readonly y: number

constructor(x: number, y: number) {
this.x = x
this.y = y
}
}

function newPoint(
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y)
}

const point: Point = newPoint(Point2D, 1, 2)

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

使用泛型创建对象

在了解完构造签名和构造函数类型之后,下面我们来开始解决开头部分 GenericCreator<T> 示例当中 T 被用作值的问题,首先我们需要重构一下 create 方法,具体如下所示

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

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

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

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

更新完 GenericCreator 泛型类,我们就可以使用我们新的 create 方法来创建 FirstClassSecondClass 类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FirstClass {
id: number | undefined
}

class SecondClass {
name: string | undefined
}

class GenericCreator<T> {
create<T>(c: { new(): T }): T {
return new c()
}
}

const creator1 = new GenericCreator<FirstClass>()
const firstClass: FirstClass = creator1.create(FirstClass)

const creator2 = new GenericCreator<SecondClass>()
const secondClass: SecondClass = creator2.create(SecondClass)

现在可以发现,程序已经可以正常运行

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×