泛型
定义一个泛型函数:
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)[]
映射类型
前面已经说过,可以使用索引签名约束对象类型的属性:
type Booleans = {
[attr:string]: boolean;
}
const CheckList: Booleans = {
check1: true,
check2: false,
check3: true,
}
在索引签名中,除了直接定义索引名称的类型([attr:string]),还可以将索引名称映射到某个类型的索引类型联合:
type Booleans2<T> = {
[attr in keyof T]: boolean;
}
type Fish = {
name: string;
swim: () => void;
}
type CheckList2 = Booleans2<Fish>; // 类型推断为 { name: boolean; swim: boolean; }
映射修饰符
在进行类型映射的时候,可以使用属性修饰符readonly和?,并且可以使用+或-添加或删除对应的修饰符:
type ReadonlyPerson = {
readonly name: string;
readonly age: number;
}
type Writable<T> = {
-readonly [key in keyof T]: T[key];
}
type WritablePerson = Writable<ReadonlyPerson>;
const tom: WritablePerson = { name: 'Tom', age: 20 };
tom.age = 21;
对于修饰符?,是类似的:
type OptionalPerson = {
name: string;
age?: number;
career?: string;
}
type Full<T> = {
[key in keyof T]-?: T[key];
}
type FullPerson = Full<OptionalPerson>; // 类型推断为 { name: string; age: number; career: string; }
as
从 TS 4.1 开始,可以使用关键字as在类型映射时重定义键名:
type Getter<T> = {
[key in keyof T as `get${Capitalize<string & key>}`]: () => T[key];
}
type Car2 = {
brand: string;
model: string;
}
type CarGetter = Getter<Car2>; // 类型推断为 { getBrand(): string; getModel(): string; }
Capitalize是一个 TS 的预定义类型,其用途是将字符串类型的首字母大写。比如Capitalize<'hello'>的结果是'Hello'类型。
在上面这个示例中,利用重定义键名,定义了一个将指定类型转换为 Getter 类型的Getter。
另外一种应用是排除特定的属性:
type NoId<T> = {
[key in keyof T as key extends 'id' ? never : key]: T[key];
}
type Person3 = {
id: number;
name: string;
age: number;
}
type PersonWithoutId = NoId<Person3>; // 类型推断为 { name: string; age: number; }
被映射的类型还可以是任意类型的联合,比如:
type HandleEvents<Events extends {kind:string}> = {
[Event in Events as Event['kind']]: (event: Event) => void;
}
type SquareEvent = {
kind: 'square';
size: number;
}
type RectangleEvent = {
kind: 'rectangle';
width: number;
height: number;
}
type EventHandlers = HandleEvents<SquareEvent | RectangleEvent>;
// 类型推断为 {
// square: (event: SquareEvent) => void;
// rectangle: (event: RectangleEvent) => void;
// }
模版字面量类型
可以像在普通 JS 代码中那样在 TS 的类型上下文中使用模版字符串构造字符串类型:
type Hello = 'Hello';
type HelloWorld = `${Hello} World`; // 类型推断为 "Hello World"
与普通 JS 代码不同的是,可以在模版字符串中使用类型联合:
type Hello2 = 'Hello';
type Bye2 = 'Bye';
type Jack = 'Jack';
type Tom = 'Tom';
type Message = `${Hello2 | Bye2} ${Jack | Tom}`;
// 类型推断为 "Hello Jack" | "Hello Tom" | "Bye Jack" | "Bye Tom"
最终的字符串类型是原始字符串类型组合的笛卡尔积。
类型中的字符串联合
看一个示例:
type WatchableObject ={
on(eventName: string, callback: () => void): void;
}
function makeWatchedObject<T extends object>(obj: T): T & WatchableObject{
const newObj = { obj};
(newObj as WatchableObject).on = (eventName: string, callback: () => void) => {
console.log(`Event: ${eventName}`);
};
return newObj as T & WatchableObject;
}
const person = { name: 'Tom', age: 10 };
const watchedPerson = makeWatchedObject(person);
watchedPerson.on('ageChanged', () => {
console.log(`I'm now ${watchedPerson.age} years old`);
});
WatchableObject表示一种可以监视属性改变的对象,可以通过其on方法添加属性改变的监视函数:
watchedPerson.on('ageChanged', () => {
console.log(`I'm now ${watchedPerson.age} years old`);
});
函数makeWatchedObject用于具体的将普通对象转化为可以监视的对象,这只是一个简单示例,并没有真正实现属性监视,只是简单的为目标对象创建了on方法。
on方法需要指定一个事件名称和钩子函数,一般而言,事件名称是按照一定规则定义好的,比如示例中,年龄属性(age)改变的事件名称就应该是ageChanged。
如果要将这种规则约束体现在类型定义中,需要修改代码:
type WatchableObject<T> = {
on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
}
function makeWatchedObject<T extends object>(obj: T): T & WatchableObject<T>{
const newObj = {... obj};
(newObj as WatchableObject<T>).on = (eventName: string, callback: () => void) => {
console.log(`Event: ${eventName}`);
};
return newObj as T & WatchableObject<T>;
}
现在,TS 可以严格检查事件名称,不符合规则的名称就会报错:
watchedPerson.on('ageChanged', () => {
console.log(`I'm now ${watchedPerson.age} years old`);
});
watchedPerson.on('nameChanged', ()=>{
console.log(`I'm now called ${watchedPerson.name}`);
});
watchedPerson.on('schoolChanged', ()=>{ // 报错
});
watchedPerson.on('nameUpdated', ()=>{ // 报错
});
使用模版字面量进行推断
假设这里的on接收的钩子方法需要接收一个参数,这个参数表示修改后的新属性值,我们可以:
type WatchableObject<T> = {
on(eventName: `${string & keyof T}Changed`, callback: (newVal: any) => void): void;
}
但这样有点太宽泛了,可以进一步推断并约束类型,让其与被改变的属性类型一致:
type WatchableObject<T> = {
on<Key extends keyof T & string>(eventName: `${Key}Changed`, callback: (newVal: T[Key]) => void): void;
}
绑定事件时钩子函数的参数类型会被准确推断:
watchedPerson.on('ageChanged', (newVal) => { // newVal 推断为 number
console.log(`I'm now ${newVal} years old`);
});
watchedPerson.on('nameChanged', (newVal) => { // newVal 推断为 string
console.log(`I'm now called ${newVal}`);
});
内置字符串操作类型
TS 内置一些类型,用于对字符串类型进行一些常见操作,这些预定义类型内置在 TS 的编译器中。
Uppercase用于将字符串转换为全大写:
type HelloWorld2 = 'Hello World';
type UppercaseHelloWorld = Uppercase<HelloWorld2>; //类型推断为 "HELLO WORLD"
Lowercase用于将字符串转换为全小写:
type LowercaseHelloWorld = Lowercase<HelloWorld2>; // 类型推断为 "hello world"
Capitalize用于将字符串转换为首字母大写:
type HelloWorld3 = 'hello world';
type CapitalizeHelloWorld = Capitalize<HelloWorld3>; // 类型推断为 "Hello world"
Uncapitalize用于将字符串转换为首字母小写:
type HelloWorld4 = 'Hello world';
type UncapitalizeHelloWorld = Uncapitalize<HelloWorld4>; // 类型推断为 "hello world"
The End.
参考资料

文章评论