泛型 (Generics
)是 TypeScript
中用于创建可复用组件的功能
-
它可以在类、函数、接口等定义中使用,使它们能够处理不同类型的数据,而不局限于某一种特定的数据类型
-
泛型可以在不丢失类型检查的前提下,编写可扩展和复用性高的代码
认识
一个需求:封装一个函数,传入一个参数,并且返回这个参数
-
如果是
TypeScript
的思维方式,要考虑这个参数和返回值的类型需要一致 -
虽然
any
是可以的,但是定义为any
的时候,其实已经丢失了类型信息tsfunction fn(arg: any) :any { return arg } fn(123) // function fn(arg: any): any,不能根据传入的确定类型
-
需要在这里使用一种特性的变量 - 类型变量(
type variable
),它作用于类型而不是值 -
这就有了泛型,在开发中用的也比较多
使用
-
定义时使用
<Type>
可以多个,使用时通过<类型>
的方式将类型传递给函数、接口等 -
Type
是一个类型变量,表示一个占位符,调用时由传入参数的类型决定 -
常见的占位符名称:
-
T
:Type
的缩写,类型 -
K、V
:key
和value
的缩写,键值对 -
E
:Element
的缩写,元素 -
O
:Object
的缩写,对象
-
-
理论比较模糊下面看代码具体学习泛型的应用
泛型函数
使用泛型定义一个函数,它能够根据传入的参数类型自动推断类型,而不是固定为某一种类型
-
基本使用:
tsfunction genericsFn1<T>(arg: T): T { // 说明这个函数的参数和返回值都是调用函数时传入的类型 <类型> return arg } // <string> 这里的string会传递给函数什么中的T,函数genericsFn就为 function genericsFn<string>(arg: string): string console.log(genericsFn1<string>('stringType'))
-
多个参数:
tsfunction genericsFn2<T, V>(msg: T, value: V): V { console.log(msg) return value } // 不传泛型会进行类型推断 function genericsFn2<string, 123>(msg: string, value: 123): 123 console.log(genericsFn2('hello', 123)) console.log(genericsFn2<string>('hello', 123)) // 因为写了两个,只传一个会报错,但写了默认类型就不会报错 // function genericsFn2<string, number>(msg: string, value: number): number console.log(genericsFn2<string, number>('hello', 123))
-
默认类型:
tsfunction genericsFn3<T, V = number>(msg: T, value: V): V { console.log(msg) return value } // 不传就使用默认类型:function genericsFn3<string, number>(msg: string, value: number): number console.log(genericsFn3<string>('hello', 123))
泛型类
类似于泛型函数,类也可以定义泛型,从而能够处理不同类型的数据
-
基本使用:
tsclass Shape1<T> { constructor(public x: T, public y: T){ this.x = x this.y = y } getArea(value: T): void{ console.log(value) } } const s1 = new Shape1('10', '23') // constructor Shape<string>(x: string, y: string): Shape<string> s1.getArea('90') // Shape1<string>.getArea(value: string): void const s2 = new Shape1<number>(10, 20) s2.getArea(30) // Shape1<number>.getArea(value: number): void
-
多个类型:
tsclass Shape2<T, U> { constructor(public x: T, public y: U){ this.x = x this.y = y } getArea<V>(value: V): void{ console.log(value, this.x, this.y) } } const s1 = new Shape2(10, 20) s1.getArea('30') const s2 = new Shape2<number, string>(10, '20') s2.getArea<number>(30)
-
默认参数:
tsclass Shape3<T, U = string> { constructor(public x: T, public y: U){ this.x = x this.y = y } getArea<V = number>(value: V): void{ console.log(value, this.x, this.y) } } const s1 = new Shape3<number>(10, '20') s1.getArea(30) class Circle<T> extends Shape3<number> { constructor(x: number, y: string, public r:T) { super(x, y) this.r = r } } const c = new Circle(10, '20', 30)
泛型接口
也可以用泛型来定义接口和别名,使接口和别名能够接受不同的类型参数
-
基本使用:
tstype IFoo<T> = { initValue: T valueList: T[] hanleValue: (value: T) => void } interface IFoo1<T> { initValue: T valueList: T[] hanleValue: (value: T) => void } /* 泛型接口传入number就为: interface IFoo1<number> { initValue: number valueList: number[] hanleValue: (value: number) => void } */ const foo1: IFoo1<number> = { initValue: 0, valueList: [1,2,3], hanleValue(value) { // 这里value不写类型也能推出是number console.log(value) } }
-
多个类型:
tsinterface IFoo2<T, V> { initValue: T valueList: V[] hanleValue: (value: T) => V } const foo2: IFoo2<number, string> = { initValue: 0, valueList: ['abc', 'cba'], hanleValue(value) { return value.toString() } }
-
默认类型:
tsinterface IFoo3<T = number, V = string> { initValue: T valueList: V[] hanleValue: (value: T) => V } const foo3: IFoo3 = { initValue: 0, valueList: ['abc', 'cba'], hanleValue(value) { return value.toString() } }
泛型约束
在某些情况下,可能希望限制泛型只能是某些特定类型,这时可以使用泛型约束(Generic Constraints
) ,通过 <T extends SomeType>
给泛型添加条件限制 ,T
必须是 SomeType
或其子类型,约束确保了泛型具有特定的属性或行为
-
约束属性和方法
-
比如
string
和array
都是有length
的,或者某些对象也是会有length
属性的 -
那么只要是拥有
length
的属性都可以作为我们的参数类型,那么应该如何操作呢?
ts// interface ILength { // length: number // } function foo<T extends { length: number }>(msg: T): T{ return msg } foo({name: 'obj', length: 1}) foo<(string | number)[]>(['abc', 123]) foo<string>('abc')
-
-
约束特定类型
tsfunction compare<T extends number | string>(a: T, b: T): boolean { return a === b; } compare(10, 20); // 可以 compare("hello", "world"); // 可以 // compare(true, false); // 错误:boolean 不属于 number | string
-
约束数组或对象的索引
tsfunction getValue<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } const person = { name: "Alice", age: 25 }; const name = getValue(person, "name"); // 返回 "Alice" // const invalid = getValue(person, "height"); // 错误: 类型 'height' 不存在于 'person'
-
约束类
tsclass Animal { name: string; constructor(name: string) { this.name = name; } } class Dog extends Animal { breed: string; constructor(name: string, breed: string) { super(name); this.breed = breed; } } function printAnimal<T extends Animal>(animal: T): void { console.log(animal.name); } const dog = new Dog("Rex", "Golden Retriever"); printAnimal(dog); // 输出: Rex
映射类型
允许你基于现有的类型进行转换或构造新类型。通过映射类型你可以对类型的所有属性应用相同的变换规则,例如使属性变为可选、只读等
-
大部分内置的工具和类型体操题目都是通过映射类型来实现的
-
映射类型主要使用
in
和keyof
关键字完成,获取对象类型所有键,然后循环遍历键名创建一个类型 -
其中
keyof
用于获取某个对象类型的所有键并返回它们的联合类型 -
in
关键字主要用于循环遍历枚举一个类型的键
基本使用:
ts
// keyof作用
type Person = { name: string; age: number; address: string; };
type PersonKeys = keyof Person; // 'name' | 'age' | 'address'
interface IPerson1 {
name: string
age: number
}
type MapType1<T> = {
[K in keyof T]: T[K]
}
type newMap1 = MapType1<IPerson1> // type newMap1 = { name: string; age: number; }
添加修饰符:
ts
interface IPerson1 {
name: string
age: number
}
type MapType2<T> = {
[K in keyof T]?: T[K]
}
type newMap2 = MapType2<IPerson1> // type newMap2 = { name?: string | undefined; age?: number | undefined; }
type MapType3<T> = {
readonly [K in keyof T]: T[K]
}
type newMap3 = MapType3<IPerson1> // type newMap3 = { readonly name: string; readonly age: number; }
type MapType4<T> = {
readonly [K in keyof T]?: T[K]
}
type newMap4 = MapType4<IPerson1> // type newMap4 = { readonly name?: string | undefined; readonly age?: number | undefined; }
减少修饰符: 可以通过前缀 - 或者 +
删除或者添加这些修饰符,如果没有写前缀,相当于使用了 +
前缀
ts
interface IPerson2 {
name?: string
age?: number
}
type MapType5<T> = {
[K in keyof T]-?: T[K]
}
type newMap5 = MapType5<IPerson1> // type newMap5 = { name: string; age: number; }
条件类型
很多时候,日常开发中需要基于输入的值来决定输出的值,同样也需要基于输入值的类型来决定输出值的类型 ,条件类型(Conditional types
)就是用来帮助我们描述输入类型和输出类型之间的关系
语法 :T extends U ? X : Y
-
T
:输入的类型 -
U
:判断的目标类型 -
X
:当T
是U
的子类型时使用的类型 -
Y
: 当T
不是U
的子类型时使用的类型
ts
function sum<T extends number | string>(arg1: T, arg2: T): T extends string ? string : number
function sum (arg1: any, arg2: any){
return arg1 + arg2
}
console.log(sum<string>('aaa', 'bbb')) // aaabbb
console.log(sum<number>(10, 20)) // 30
推断
条件类型提供了 infer
关键词,可以从正在比较的类型中推断类型,然后在 true
分支里引用该推断结果
-
语法 :
T extends U ? infer V : X
-
T
:输入的类型 -
U
:判断的目标类型 -
infer V
:在满足条件时推断出的类型变量V
-
X
:如果条件不成立时使用的类型
-
-
案例:
-
假设我们有一个数组类型,希望从中提取出其元素的类型
tstype ElementType<T> = T extends (infer U)[] ? U : T; type A = ElementType<number[]>; // number type B = ElementType<string[]>; // string type C = ElementType<any[]> // any type D = ElementType<(number | string)[]> // string | number type E = ElementType<number>; // number (非数组类型直接返回自己)
-
假设我们有一个函数类型,希望从中提取出返回值类型,可以使用
infer
tstype ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never type A = ReturnType<() => string> // string type B = ReturnType<() => any[]> // any[] type C = ReturnType<() => number | string> // string | number type D = ReturnType<number> // never 不是函数类型报错
为什么使用
never
?-
当条件不满足时,想明确表示无效类型 或永远不会发生的情况,这时使用never最合适
-
传入的
T
不是一个函数类型时,无法提取出返回值类型。这时返回never
表示:这里不会有有效的返回值类型,因为这个条件根本不成立
为什么不用
undefined
或其他类型?-
使用
never
更符合逻辑 。因为传入的类型不是函数类型,所以没有任何返回值类型可以被推断。never
表示这种不可能的情况 -
never
的语义明确:它告诉我们,这种情况不应该发生,因此无法推断出任何合理的类型
-
-
分发
分发条件类型主要用于当类型参数是联合类型时,TypeScript
会对联合类型中的每个成员单独地应用条件类型进行计算,然后将结果重新组合成一个新的联合类型
-
语法:
T extends U ? X : Y
-
当
T
是一个联合类型时,T
的每个成员都会单独进行判断,并将结果重新组合成新的联合类型 -
T
:输入的类型 -
U
:判断的目标类型 -
X
:当T
是U
的子类型时使用的类型 -
Y
: 当T
不是U
的子类型时使用的类型
-
-
使用:
tstype DistributType<T> = T extends any ? T[] : never type A = DistributType<number | string> // number[] | string[] type B = DistributType<string> // string[] type C = DistributType<() => void> // (() => void)[]
-
避免分发:
有时希望避免分发行为,而是将整个联合类型作为一个整体来进行判断,可以通过包裹类型来阻止分发 ,例如使用元组:
[T]
将整个联合类型包裹为一个整体,条件判断只会执行一次tstype Example<T> = [T] extends [string] ? string : number; type Result = Example<'hello' | 42 | true>; // number
内置工具
Partial<Type>
用于构造一个Type
下面的所有属性都设置为可选的类型
ts
type OType = {
name: string,
age: number
}
type PartialType1 = Partial<OType> // type PartialType1 = { name?: string | undefined; age?: number | undefined; }
// 使用映射类型自己实现
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type PartialType2 = MyPartial<OType> // type PartialType1 = { name?: string | undefined; age?: number | undefined; }
Required<Type>
用于构造一个Type
下面的所有属性全都设置为必填 的类型,这个工具类型跟 Partial
相反
ts
type OType = {
name: string,
age?: number
}
type RequiredType1 = Required<OType> // type RequiredType1 = { name: string; age: number; }
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
type RequiredType2 = MyRequired<OType> // type RequiredType2 = { name: string; age: number; }
Readonly<Type>
用于构造一个Type
下面的所有属性全都设置为只读的类型,意味着这个类型的所有的属性全都不可以重新赋值
ts
type OType = {
name: string,
age?: number
}
type ReadonlyType1 = Readonly<OType> // type ReadonlyType1 = { readonly name: string; readonly age?: number | undefined;}
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
type ReadonlyType2 = MyReadonly<OType> // type ReadonlyType2 = { readonly name: string; readonly age?: number | undefined;}
Record<Keys, Type>
用于构造一个对象类型,它所有的key
(键)都是Keys
类型,它所有的value
(值)都是Type
类型
ts
type RecordType1 = Record<'上海' | 'aaa', number | string> // type RecordType1 = {aaa: string | number; 上海: string | number;}
type KeyOfAny = keyof any; // 等价于:string | number | symbol
type MyRecord<K extends keyof any, T> = {
[P in K]: T
}
type RecordType2 = MyRecord<'上海' | 'aaa', number | string> // type RecordType2 = {aaa: string | number; 上海: string | number;}
keyof any
它表示所有可能的键的联合类型 ,获取到的是string | number | symbol
,因为keyof
作用是获取某个对象类型的所有键 , 后面写any
时表示是任何类型对象、数组、函数等等,但无论它是什么类型,获取对象的键名只能是string
、number
或symbol
Pick<Type, Keys>
用于构造一个类型,它是从Type
类型里面挑了一些属性Keys
ts
type OType = {
name: string,
age?: number
}
type PickType1 = Pick<OType, 'age'> // type PickType1 = { age?: number | undefined;}
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type PickType2 = MyPick<OType, 'age'> // type PickType2 = { age?: number | undefined;}
Omit<Type, Keys>
用于构造一个类型,它是从Type
类型里面过滤了一些属性Keys
- 思路:
-
使用
keyof
获取所有的键 -
使用条件类型过滤掉需要剔除的键
-
重新构造一个新的类型,包含剩余的键
-
ts
type OType = {
name: string,
age?: number
}
type OmitType1 = Omit<OType, 'age'> // type OmitType1 = { name: string; }
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
type OmitType2 = MyOmit<OType, 'age'> // type OmitType2 = { name: string; }
-
keyof T
:获取类型T
的所有键名 -
条件类型
P extends K ? never : P
:如果键P
是要剔除的键K
,那么在结果中设置为never
,否则保留
Exclude<UnionType, ExcludedMembers>
用于构造一个类型,它是从UnionType
联合类型里面排除了所有可以赋给ExcludedMembers
的类型
ts
type ExcludeType1 = Exclude<'上海' | 'aaa', '上海'> // type ExcludeType1 = "aaa"
type MyExclude<T, K> = T extends K ? never : T
type ExcludeType2 = MyExclude<'上海' | 'aaa', '上海'> // type ExcludeType2 = "aaa"
T extends K
:判断T
中的每个成员是否是K
的子类型 ,如果成立,就返回never
,否则返回T
Extract<Type, Union>
用于构造一个类型,它是从Type
类型里面提取了所有可以赋给Union
的类型
ts
type ExtractType1 = Extract<'上海' | 'aaa' | 'bbb', '上海' | 'aaa'> // type ExtractType1 = "aaa" | "上海"
type MyExtract<T, K> = T extends K ? T : never
type ExtractType2 = MyExtract<'上海' | 'aaa' | 'bbb', '上海' | 'aaa'> // type ExtractType2 = "aaa" | "上海"
NonNullable<Type>
用于构造一个类型,这个类型从Type
中排除了所有的null、undefined
的类型
- 现象:
- 在
TypeScript
中,'aaa'
是一个字面量类型 ,而string
是一个宽泛类型 - 当你将字面量类型与其宽泛类型(例如
'aaa'
和string
)联合在一起时,TypeScript
会进行类型合并
- 在
ts
type NonNullableType = NonNullable<any[] | 'aaa' | null | number> // type NonNullableType = number | any[] | "aaa"
type NonNullableType1 = NonNullable<any[] | 'aaa' | null | number | undefined | string> // type NonNullableType = string | number | any[]
type MyNonNullable<T> = T extends null | undefined ? never : T
type NonNullableType2 = MyNonNullable<any[] | 'aaa' | null | number> // type NonNullableType2 = number | any[] | "aaa"
type NonNullableType3 = MyNonNullable<any[] | 'aaa' | null | number | undefined | string> // type NonNullableType3 = string | number | any[]
ReturnType<Type>
用于构造一个含有Type
函数的返回值的类型
ts
type A = ReturnType<() => string> // string
type B = ReturnType<() => any[]> // any[]
type C = ReturnType<() => number | string> // string | number
type D = ReturnType<number> // never 不是函数类型报错
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never
type A = MyReturnType<() => string> // string
type B = MyReturnType<() => any[]> // any[]
type C = MyReturnType<() => number | string> // string | number
type D = MyReturnType<number> // never 不是函数类型报错
InstanceType<Type>
用于构造一个由所有Type
的构造函数的实例类型组成的类型,该工具类型接受一个构造函数类型 Type
作为参数,如果 Type
是合法的构造函数类型 ,它会提取出构造函数返回的实例类型 ,如果不是合法的构造函数类型则返回 any
ts
// 创建实例的工具函数时会用到这个InstanceType
function factory1<T extends new (...args: any[]) => any>(
ctor: T,
...args: ConstructorParameters<T> // 使用 ConstructorParameters 来匹配构造函数参数
): InstanceType<T> {
return new ctor(...args); // 将参数传递给构造函数
}
const person1 = factory1(Person, "Alice", 25); // const person1: Person
const student1 = factory1(Student, "清华"); // const student1: Student
type MyInstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : never
function factory2<T extends new (...args: any[]) => any>(
ctor: T,
...args: ConstructorParameters<T> // 使用 ConstructorParameters 来匹配构造函数参数
): MyInstanceType<T> {
return new ctor(...args); // 将参数传递给构造函数
}
const person2 = factory1(Person, "Alice", 25); // const person2: Person
const student2 = factory1(Student, "清华"); // const student2: Student
类型体操
-
类型系统其实在很多语言里面都是有的,比如
Java、Swift、C++
等等,但是相对来说TypeScript
的类型非常灵活 -
因为
TypeScript
的目的是为JavaScript
添加一套类型校验系统,JavaScript
本身的灵活性,让TypeScript
类型系统不得不增加附加的功能以适配JavaScript
的灵活性,所以TypeScript
是一种可以支持类型编程的类型系统 -
如果你在开发一些框架、库,或者通用性的工具,为了考虑各种适配的情况,就需要使用类型编程
-
很多开发者为了进一步增强自己的
TypeScript
编程能力,还会专门去做一些类型体操的题目,下面是题目链接可以自行练习: