一、 泛型是什么
软件工程中,我们不仅要创建一致的定义良好的 API ,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
—— 官方文档介绍
官方文档说的有点晕,不过既然介绍提到了 Java ,那就看看泛型在 Java 的解释:
Java 泛型是 J2 SE1.5 中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 —— 百度百科
对于参数我们就比较熟悉了,在定义函数的时候写入形参,后面调用的时候再传入具体的实参;同样的,参数化类型也就是将原先的具体的类型当做一个参数来处理,在定义阶段就相当于定义函数时候的形参一样,没有指定的类型,只是相当于一个占位符的作用,而后在使用阶段的时候根据传入的类型来确定。
也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,也就被分别称为泛型类、泛型接口、泛型方法。
二、泛型怎么用
说完概念,来看看一个简单的泛型怎么创建。
- 泛型方法
function fn1<T>(value: T): T {
return value
}
const value = fn1<number>(3)
按照上面的理解,在定义 fn1
方法的时候,我们写入了形参 value
和泛型 <T>
, T
被称为类型变量,它可以在函数内作为类型占位符使用,这里被指定为参数 value
和方法 fn1
的返回值的类型。
当我们在调用方法的时候, fn1<number>(3)
就相当于是传入了函数的实参 3
以及类型变量的值 number
,这是显式的设定了类型变量的值,不过通常在调用的时候可以隐式设定,也就是忽略尖括号 fn1(3)
,编译器会自动去识别类型变量与参数的关系,给类型变量赋值。
类型变量的数量是任意的,需要几个就定义几个,类型变量之间用逗号分隔 <T, ...>
:
function fn2<T, U>(value: T, message: U): [T, U] {
return [value, message]
}
- 泛型接口
泛型还可以用在接口上,也就是泛型接口:
interface Fn<V, M> {
message: M
value: V
}
function fn3<T, U>(value: T, message: U): Fn<T, U> {
return {
message,
value
}
}
- 泛型类
泛型在描述类的时候就是泛型类:
class Queue {
private data = []
push = item => this.data.push(item)
pop = () => this.data.shift()
}
上面的代码是队列的简单实现,假如要求队列元素都是 number
类型,明显这样写就是不足够的,因为现在并没有指定类型,TypeScript 并不会去检查。一个简单的修改方法就是再添加元素的时候限制类型:
class QueueNumber {
private data = []
push = (item: number) => this.data.push(item)
pop = () => this.data.shift()
}
const numberQueue = new QueueNumber()
numberQueue.push(1)
numberQueue.push('34') // 错误 提示类型不一致
如果是需要两个队列,一个是 number
类型,一个是 string
类型,那该怎么搞?这时就需要泛型了:
class QueueNumber<T> {
private data: T[] = []
push = (item: T) => this.data.push(item)
pop = () => this.data.shift()
}
const numberQueue = new Queue<number>()
numberQueue.push(2)
const stringQueue = new Queue<string>()
stringQueue.push('2')
三、泛型参数的默认类型
TypeScript 2.3 之后,可以给参数类型指定默认类型。
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = []
for (let i = 0; i < length; i++) {
result[i] = value
}
return result
}
四、泛型约束与继承
当我们使用泛型的时候,由于并没有确定的类型,所以不能随意的操作它的属性和方法,限制了我们随意的操作对象导致的一些问题,这就是泛型的约束作用。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length) // 错误 类型“T”上不存在属性“length”
return arg
}
可以定义一个接口来描述约束条件,通过继承 extends
这个接口实现:
泛型的继承和接口类似。
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length)
return arg
}
多个类型参数情况下的约束:
function copyKeysBad<T , U>(target: T, source: U): T {
for (let key in source) {
target[key] = source[key] // 错误 类型T不存在类型U的属性
}
return target
}
// 类型T继承类型U 保证U上不会出现T中不存在的字段
function copyKeys<T extends U, U>(target: T, source: U): T {
for (let key in source) {
target[key] = (<T>source)[key]
}
return target
}
let x = { a: 1, b: 2, c: 3, d: 4 }
copyKeys(x, { b: 10, d: 20 })
五、泛型条件
在 TypeScript 2.8 中引入了条件类型,根据条件得到不同的类型。
// 类似三元运算
// 如果类型T是类型U的子集 那么类型是X 否则是Y
T extends U ? X : Y
泛型条件也可以嵌套,和 if
语句、三元运算一样,可以嵌套。
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string> // "string"
type T1 = TypeName<"a"> // "string"
type T2 = TypeName<true> // "boolean"
type T3 = TypeName<() => void> // "function"
type T4 = TypeName<string[]> // "object"
分布式条件类型
条件类型还有一个特性:分布式条件类型。在结合联合类型使用时(extends
左边的联合类型),分布式条件类型会被自动分发成联合类型。
type newType = T extends U ? X : Y
// 如果 T 的类型是 A|B|C
// 会被解析为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
type T10 = TypeName<string | (() => void)> // "string" | "function"
type T12 = TypeName<string | string[] | undefined> // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]> // "object"
分布式条件类型的前提是待检查的类型必须是裸类型(naked type parameter
),即没有被数组,元组或者函数包裹。
// naked type
type NakedType<T> = T extends boolean ? "yes" : "no"
type DistributedUsage = NakedType<number | boolean> // "yes" | "no"
// wrapped type
type WrappedType<T> = Array<T> extends Array<boolean> ? "yes" : "no"
type NonDistributedUsage = WrappedType<number | boolean> // "no"
infer
关键字
条件类型还可以在条件判断中声明泛型类型。通过使用 infer
关键字。
type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : T
type Foo = FunctionReturnType<() => void> // void
type Bar = FunctionReturnType<(name: string) => string> // string
type Buz = FunctionReturnType<(name: string, other: string) => boolean> // boolean
type Str = FunctionReturnType<string> // string
infer R
就是在条件判断中声明的新的泛型,它会根据实际传入的泛型类型推断出该泛型的具体类型。
协变和逆变
关于逆变和协变的具体内容,这里先不具体展开,这两个概念也只是说到这里才刚认识,还没搞懂,后面会再详细深入。这里先简单的了解一下。
协变就是让泛型接口可以接受一个更加具体的接口作为参数或返回值;逆变就是让接口的参数类型或返回值更加具体。逆变就是对具体成员的输入参数进行一次类型转换,逆变就是对具体成员的输入参数进行一次类型转换。
// 在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>; // string
type T11 = Foo<{ a: string, b: number }>; // string | number
// 在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
六、内置常用的泛型
Partial
Partial<T>
的作用就是将某个类型里的属性全部变为可选项 ?
type Partial<T> = {
[P in keyof T]?: T[P]
}
Record
Record<K extends keyof any, T>
的作用是将 K
中所有的属性的值转化为 T
类型
type Record<K extends keyof any, T> = {
[P in K]: T
}
Pick
Pick<T, K extends keyof T>
的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
Exclude
Exclude<T, U>
的作用是将某个类型中属于另一个的类型移除掉
type Exclude<T, U> = T extends U ? never : T
ReturnType
ReturnType<T>
的作用是用于获取函数 T
的返回类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。