泛型
定义一个泛型函数:
function returnParameter<T>(param: T): T {
return param;
}
调用函数:
returnParameter<string>('hello');
调用泛型函数时并不是一定要指定泛型类型,也可以省略泛型类型,让 TS 编译器根据实际参数的类型推断:
returnParameter('world');
但有时候遇到复杂的调用,TS 无法正确推断出泛型类型,就需要手动指定。
泛型函数类型
在 TS 中,可以用函数类型来约束函数对象,同样的,如果函数对象指向一个泛型函数,可以用泛型函数类型进行约束和定义:
let returnParamFunc: <T>(param: T) => T = returnParameter;
泛型函数类型和普通的函数类型类似,只不过需要用<...>指定泛型参数。
除了泛型函数类型,也可以用可调用对象的方式描述泛型函数类型:
let returnParamFunc2: { <T>(param: T): T } = returnParameter;
或者用interface定义一个可调用对象,再进行函数类型声明:
interface ReturnParameterFunc {
<T>(param: T): T;
}
let returnParamFunc3: ReturnParameterFunc = returnParameter;
泛型类
泛型类的定义方式与interface类似:
class GenericNumber<T> {
zeroValue?: T;
add?: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };
console.log(myGenericNumber.add(1, 2));
泛型约束
有时候,你希望泛型函数中的泛型类型具备某种特性:
function loggingIdentity2<T>(arg: T): T {
console.log(arg.length); // 报错,类型“T”上不存在属性“length”。
return arg;
}
这里希望泛型类型的参数arg必须拥有length属性。
可以自定义一个拥有length属性的类型,并使用这个类型约束泛型参数的范围:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
console.log(loggingIdentity<string>('string'));
console.log(loggingIdentity<number>(1)); // 报错,类型“number”不满足约束“Lengthwise”
console.log(loggingIdentity<boolean>(true)); // 报错,类型“boolean”不满足约束“Lengthwise”
console.log(loggingIdentity<{ length: number }>({ length: 10 }));
类型参数约束
可以用另一个泛型参数约束当前的泛型参数,比如:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
const obj = { 'a': 1, 'b': 2, 'c': 3 }
console.log(getProperty(obj, 'a'));
console.log(getProperty(obj, 'd')); // 报错,类型“"d"”的参数不能赋给类型“"a" | "b" | "c"”的参数
这里的泛型参数Key指定为泛型参数Type的key。
在泛型中使用类类型
看一个在 JS 中使用工厂模式创建对象的示例:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
function createPerson(name: string): Person {
return new Person(name);
}
const p = createPerson('张三');
这里createPerson是一个工厂函数,用于创建Person对象。
当然一般会有更复杂的抽象层次,比如:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Student extends Person {
id: number;
constructor(name: string, id: number) {
super(name);
this.id = id;
}
}
class Teacher extends Person {
courseName: string;
constructor(name: string, courseName: string) {
super(name);
this.courseName = courseName;
}
}
如果依然需要用一个工厂函数去创建Person的不同派生类对象,就需要指定派生类的类型和构造参数:
const p = createPerson(Person, '张三');
const s = createPerson(Student, '张三', 1);
const t = createPerson(Teacher, '张三', 'Javascript');
这样做是可行的,因为 JS 中类类型本身也是一个对象,可以作为参数传递。
此时的工厂函数使用泛型表示接收的类类型,并用基类进行约束:
function createPerson<T extends Person>(constractor: { new(args: any[]): T }, args: any[]): T {
return new constractor(args);
}
这里表示类类型的参数constractor,使用{ new(...args: any[]): T }进行类型定义,它说明了对应的类类型的构造函数是怎样的。
keyof
运算符keyof作用于一个对象类型,返回对象类型所有属性名称的联合:
type Perosn2 = { name: string; age: number; job: string };
type PersonKeys = keyof Perosn2;
const key: PersonKeys = 'name';
const key2: PersonKeys = 'age';
const key3: PersonKeys = 'job';
const key4: PersonKeys = 'sex'; // 报错
这里keyof Perosn2返回的类型实际上是name|age|job。
如果对象类型使用了索引签名,则keyof获取到的是索引名称中定义的类型:
type numberAttrObj = {[key: number]: any};
type keys = keyof numberAttrObj; // 类型推断为 number
const key5: keys = 1;
const key6: keys = 2;
比较特殊的是:
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // 类型推断为 string | number
const key7: M = 1;
这里M的类型是string|number而不是string。这是因为虽然索引签名中定义了属性名称都是string类型,但在 JS 中,你依然可以使用数字进行索引,比如a[0],此时数字索引会被强行转化为字符串索引(a['0'])。
typeof
JS 在表达式上下文中,提供一个操作符typeof,可以获取变量的类型:
function printParam(param: any){
if(typeof param === 'string'){
console.log(param.toUpperCase());
}
else if(typeof param === 'number'){
console.log(param.toFixed(2));
}
else{
console.log(param);
}
}
printParam('hello');
printParam(123.456);
printParam({});
TS 中,可以在类型上下文中使用typeof获取类型,比如:
let s1 = "hello";
let s2: typeof s1 = "world";
这里s2的类型使用的是typeof s1,表示和s1的类型相同。
这样做或许显得有点画蛇添足,但在复杂类型中,typeof是有用途的,比如:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; // 类型推断为 boolean
这里的ReturnType是一个预定义类型,可以利用它获取到特定函数类型的返回值类型。但需要注意的是,这里只能通过泛型参数指定函数类型,而不是函数名称:
function isOk(x: unknown){
return x === 'ok';
}
type K2 = ReturnType<isOk>; // 报错
可以使用typeof:
type K3 = ReturnType<typeof isOk>;
索引访问类型
可以用索引的方式使用对象类型的属性类型:
type Person2 = { name: string; age: number };
type Name = Person2['name']; // 类型推断为 string
type Age = Person2['age']; // 类型推断为 number
这里Person2['name']表示对象类型Person2的属性name的类型。
在索引中可以使用任意类型,包括类型联合:
type NameOrAge = Person2['name' | 'age']; // 类型推断为 string | number
type NameOrAge2 = Person2[keyof Person2];
特殊的,可以使用[number]的方式获取数组元素的类型:
const myArray = [{name:'xiaoming',age:18},{name:'zhangsan',age:16}]
type MyArrayElement = typeof myArray[number]; // 类型推断为 {name: string; age: number;}
type ElementName = MyArrayElement['name']; // 类型推断为 string
type ElementAge = MyArrayElement['age']; // 类型推断为 number
条件类型
可以在类型上下文中使用条件表达式:
interface Animal{
eat(food:any):void;
}
interface Dog extends Animal{
bark():void;
}
interface Car{
drive():void;
}
type DogIsAnimal = Dog extends Animal ? true : false; // 类型推断为 true
type CarIsAnimal = Car extends Animal ? true : false; // 类型推断为 false
这里的类型条件表达式Dog extends Animal ? true : false,如果条件成立(Dog是Animal的扩展类型),表达式的结果就是:前的类型(true),否则就是:后边的类型(false)。
看一个例子:
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
if (typeof nameOrId === 'string') {
return { name: nameOrId };
}
return { id: nameOrId };
}
const a = createLabel('xiaoming');
这里用重载的方式实现了createLabel函数,它可以根据不同类型的参数返回IdLabel或NameLabel。
可以使用泛型和条件类型表达式重写这个示例,这样就不需要使用函数重载:
type IdOrNameLabel<T extends string | number> = T extends string ? NameLabel : IdLabel;
function createLabel2<T extends string | number>(nameOrId: T): IdOrNameLabel<T> {
if (typeof nameOrId === 'string') {
return { name: nameOrId } as IdOrNameLabel<T>;
}
return { id: nameOrId } as IdOrNameLabel<T>;
}
const b = createLabel2('xiaoming');
const c = createLabel2(1);
console.log(b, c);
这里的
as IdOrNameLabel<T>是因为 TS 无法正确推断出类型而添加。
条件类型约束
如果要设计一个获取指定类型指定属性的类型别名:
type MessageOf<T> = T['message']; // 报错,类型“T”上不存在属性“message”。
这样会报错,因为不能确保泛型参数T一定具备指定属性,因此需要对泛型参数进一步约束:
type MessageOf2<T extends { message: unknown }> = T['message'];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf2<Email>; // 类型推断为 string
这样是没有问题的,但MessageOf2只能用于满足约束T extends { message: unknown }的类型,如果我们要用于任意类型,就需要使用类型条件表达式:
type MessageOf3<T> = T extends { message: unknown } ? T['message'] : never;
type EmailMessageContents2 = MessageOf3<Email>; // 类型推断为 string
type DogMessageContents = MessageOf3<Dog>; // 类型推断为 never
infer
在上面的示例中,使用索引的方式获取对象类型中的指定属性类型,除了这种方式,还可以使用infer关键字:
type MessageOf4<T> = T extends {message: infer Message} ? Message : never;
type EmailMessageContents3 = MessageOf4<Email>; // 类型推断为 string
type DogMessageContents2 = MessageOf4<Dog>; // 类型推断为 never
在 TS 中,infer关键字可以用于类型占位符,示例中的infer Message表示Message是message属性的类型。
再看一个示例,用infer获取数组元素类型:
type ArrayElement<T> = T extends Array<infer U> ? U : never;
const arr = [1, 2, 3];
type ArrayElementType = ArrayElement<typeof arr>; // 类型推断为 number
下面的示例是获取函数返回值类型:
type GetReturnType<T> = T extends (args: any[]) => infer R ? R : never;
function f1(): string {
return 'hello';
}
function f2(arg: number): number {
return arg;
}
function f3(){}
type F1ReturnType = GetReturnType<typeof f1>; // 类型推断为 string
type F2ReturnType = GetReturnType<typeof f2>; // 类型推断为 number
type F3ReturnType = GetReturnType<typeof f3>; // 类型推断为 void
type F4ReturnType = GetReturnType<string>; // 类型推断为 never
有一个细节,如果用GetReturnType获取重载函数的返回值,TS 最终的推断类型会是重载函数签名中最后一个签名的返回值类型:
function f4(id:number):number;
function f4(name:string):string;
function f4(arg:number|string):number|string;
function f4(arg:number|string):number|string{
return arg;
}
type F4ReturnType2 = GetReturnType<typeof f4>; // 类型推断为 number | string
分发条件类型
利用类型条件表达式定义一个类型,将给定类型转换为对应的数组类型:
type ToArray<T> = T extends any ? T[] : never;
如果给定的类型是联合类型,就会出现类型分发:
type ToArrayType = ToArray<string|number>; // 类型推断为 string[] | number[]
这里最终推断的类型是string[] | number[],而不是(string|number)[]。
TS 默认在这种情况下会进行类型分发,如果你不希望这种行为产生,可以:
type ToArray2<T> = [T] extends [any] ? T[] : never;
type ToArrayType2 = ToArray2<string|number>; // 类型推断为 (string|number)[]
The End.
参考资料

文章评论