(注明:目前泛型理解不够清晰,先照葫芦画瓢,整理一个粗略版本,后续会再次补充)
一、泛型的概念
在TS中,泛型(Generics)是一种创建可复用代码的方式,类似于代码组件的概念。具体来说,就是在定义接口、函数或类的时候,不预先指定参数、返回值的类型,而是在使用时,根据具体情况来指定相应的类型。
举个例子,定义一个函数 identity ,该函数有一个参数,函数内部就是实现了将参数原样返回,那么代码如下:
const identity = (arg) => arg;
然后我们给代码加上类型声明,并使函数的入参和返回值都应该可以是任意类型:
type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
...
上面的实现办法就是,TS有多少种类型就,写多少行代码,对应不同的类型签名,但这样会使代码冗余,难以维护。还有一种办法是使用any类型,不过这样会丧失类型检查功能,得不偿失:
const identity = (arg: any) => any;
// 哪怕返回值并没有这个方法,但依旧不会报错
identity("string").length; // ok
identity("string").toFixed(2); // ok
identity(null).toString(); // ok
如果我们想要实现:根据传递的值得类型进行推导,并根据推导出来的类型,进行类型检查,比如我传了一个string,但是返回值使用量number的方法,那么就应该报错,这种效果,就需要借助泛型来实现:
function identity<T>(arg: T): T {
return arg;
}
上面代码中的T,表示Type,是一个抽象类型,只有在调用函数时,才会确认具体的类型,这样就能适用于不同类型的数据。调用函数时,先把类型传递给
<T>
中的T,然后再链式传递给参数类型和返回值类型。
在泛型的
<>
中,我们可以定义任何数量的类型变量:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
// 调用时,使用 <> 定义好对应类型变量的类型 像传参一样 一一对应
console.log(identity<Number, string>(68, "Semlinker"));
在调用函数时,也可以省略使用
<>
显式设定类型,编译器可以自动根据我们的参数类型来得知对应类型:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
// 省略 <>
console.log(identity(68, "Semlinker"));
二、泛型约束
如果我们不对泛型的类型变量进行约束,那么其类型理论上是可以是任何类型,那这样只能使用所有类型共有的属性或方法,否则就会报错:
function trace<T>(arg: T): T {
console.log(arg.size); // 报错 Error: Property 'size doesn't exist on type 'T'
return arg;
}
所以我们要对其进行约束,方式就是通过 extends 继承一个接口:
interface Sizeable {
size: number;
}
function trace<T extends Sizeable>(arg: T): T {
console.log(arg.size);
return arg;
}
三、泛型工具类
1、概念
为了方便开发,TS内置了一些常用的工具类型,比如 Partial、Required等。不过在学习这些内置工具类型之前,我们需要先去了解一些基础的TS的特性。
2、基础特性
① typeof
typeof 在 JS 中是用来判断值的类型,在 TS 中,typeof 不仅能判断基本数据类型,还可以判断接口类型:
interface Person {
name: string;
age: number;
}
const sem: Person = { name: "semlinker", age: 30 };
console.log(typeof sem) // Person
// 获取接口类型 并赋值给 类型变量
type Sem = typeof sem; // type Sem = Person
const lolo: Sem = { name: "lolo", age: 5 }
以及获取对象的属性类型结构,作为一个类型赋值给类型变量:
const Message = {
name: "jimmy",
age: 18,
address: {
province: '四川',
city: '成都'
}
}
type message = typeof Message;
/*
type message = {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
*/
// 函数对象也可以
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // type Func = (x: number) => number[]
② keyof
keyof 操作符 是 TS 2.1 版本引入的,用来获取某种类型的所有键,其返回类型是一个联合类型:
interface Person {
name: string;
age: number;
}
// 获取接口类型的键
type K1 = keyof Person; // "name" | "age"
// 支持获取数组类型的键
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
// 支持普通数据类型的键
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"
③ in
in 用来遍历枚举类型,比如联合类型:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
④ infer(不清晰)
在条件类型语句中,可以用 infer 声明一个类型变量并使用:
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
以上代码中
infer R
就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
⑤ extends
extends 主要用于给泛型添加约束:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
⑥ 映射类型
根据已经存在的类型创建出新的类型,那被创建的新类型,我们就称为映射类型:
// 已存在的接口类型
interface TestInterface{
name:string,
age:number
}
// 处理已存在类型 并生成新类型的方法
type OptionalTestInterface<T> = {
[p in keyof T]+?:T[p]
}
// 获取新类型
type newTestInterface = OptionalTestInterface<TestInterface>
3、内置工具类型
① Partial
Partial<T>
结合keyof 和 in ,将某类型的所有属性变成可选:
// 工具类型 Partial 内部原理
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 已存在的接口类型
interface UserInfo {
id: string;
name: string;
}
// 使用Partial
type NewUserInfo = Partial<UserInfo>;
const xiaoming: NewUserInfo = {
name: 'xiaoming'
}
但 Partial 也有它的局限性:只支持对第一层属性进行处理,如果要处理的类型中有嵌套属性,则第二层往下就不会再处理。但如果想要对嵌套属性也进行处理,可以自己实现:
type DeepPartial<T> = {
// 如果是 object,则递归类型
[U in keyof T]?: T[U] extends object
? DeepPartial<T[U]>
: T[U]
};
type PartialedWindow = DeepPartial<T>; // 现在T上所有属性都变成了可选啦
② Required
Required将某类型的所有属性变成必选:
// Required 内部原理
type Required<T> = {
[P in keyof T]-?: T[P] // 其中 -? 是代表移除 ? 这个 modifier 的标识
};
③ Readonly
Readonly<T>
的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值:
// Readonly 内部原理
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 使用
interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users"
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
④ Pick
Pick 从某个类型中挑出部分属性出来:
// 原理
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 使用
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
⑤ ReturnType
ReturnType 用来得到一个函数的返回值类型:
// 原理
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R // infer在这里用于提取函数类型的返回值类型
? R
: any;
// 使用
type Func = (value: number) => string;
// ReturnType获取到 Func 的返回值类型为 string,所以,foo 也就只能被赋值为字符串了。
const foo: ReturnType<Func> = "1";
⑥ Exclude
Exclude<T, U>
的作用是将某个类型中属于另一个的类型移除掉,简单来说就是取不同的部分:
// 原理
type Exclude<T, U> = T extends U ? never : T;
// 使用
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
⑦ Extract
Extract<T, U>
的作用是从
T
中提取出
U
,简单来说就是取相同的部分:
// 原理
type Extract<T, U> = T extends U ? T : never;
// 使用
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void
⑧ NonNullable
NonNullable<T>
的作用是用来过滤类型中的
null
及
undefined
类型:
// 原理
type NonNullable<T> = T extendsnull | undefined ? never : T;
// 使用
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
⑨ Record、Omit、Parameters等