泛型
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
泛型接口
interface GenericIdentityFn<T> {
(arg: T): T
}
泛型类
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
}
泛型变量
对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:
- T(Type):表示一个 TypeScript 类型
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
泛型工具类型
typeof
typeof 的主要用途是在类型上下文中获取变量或者属性的类型,下面我们通过一个具体示例来理解一下。
interface Person {
name: string
age: number
}
const sem: Person = { name: 'semlinker', age: 30 }
type Sem = typeof sem // type Sem = Person
在上面代码中,我们通过 typeof 操作符获取 sem 变量的类型并赋值给 Sem 类型变量,之后我们就可以使用 Sem 类型:
const lolo: Sem = { name: 'lolo', age: 5 }
typeof 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:
function toArray(x: number): Array<number> {
return [x]
}
type Func = typeof toArray // -> (x: number) => number[]
keyof
该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface Person {
name: string
age: number
}
type K1 = keyof Person // "name" | "age"
type K2 = keyof Person[] // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person } // string | number
JavaScript 是一种高度动态的语言。有时在静态类型系统中捕获某些操作的语义可能会很棘手。以一个简单的 prop 函数为例:
function prop(obj, key) {
return obj[key]
}
该函数接收 obj 和 key 两个参数,并返回对应属性的值。对象上的不同属性,可以具有完全不同的类型,我们甚至不知道 obj 对象长什么样。
那么在 TypeScript 中如何定义上面的 prop 函数呢?我们来尝试一下:
function prop(obj: object, key: string) {
return obj[key]
}
在上面代码中,为了避免调用 prop 函数时传入错误的参数类型,我们为 obj 和 key 参数设置了类型,分别为 {} 和 string 类型。然而,事情并没有那么简单。针对上述的代码,TypeScript 编译器会输出以下错误信息:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
元素隐式地拥有 any 类型,因为 string 类型不能被用于索引 {} 类型。要解决这个问题,你可以使用以下非常暴力的方案:
function prop(obj: object, key: string) {
return (obj as any)[key]
}
很明显该方案并不是一个好的方案,我们来回顾一下 prop 函数的作用,该函数用于获取某个对象中指定属性的属性值。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof 操作符:
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
在以上代码中,我们使用了 TypeScript 的泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型。 是骡子是马拉出来遛遛就知道了,我们来实际测试一下:
type Todo = {
id: number
text: string
done: boolean
}
const todo: Todo = {
id: 1,
text: 'Learn TypeScript keyof',
done: false,
}
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
const id = prop(todo, 'id') // const id: number
const text = prop(todo, 'text') // const text: string
const done = prop(todo, 'done') // const done: boolean
很明显使用泛型,重新定义后的 prop<T extends object, K extends keyof T>(obj: T, key: K) 函数,已经可以正确地推导出指定键对应的类型。那么当访问 todo 对象上不存在的属性时,会出现什么情况?比如:
const date = prop(todo, 'date')
对于上述代码,TypeScript 编译器会提示以下错误:
const date = prop(todo, 'date')
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
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3) // Error, number doesn't have a .length property
这时我们需要传入符合约束类型的值,必须包含 length 属性:
loggingIdentity({ length: 10, value: 3 })
索引类型
在实际开发中,我们经常能遇到这样的场景,在对象中获取一些属性的值,然后建立对应的集合。
let person = {
name: 'musion',
age: 35,
}
function getValues(person: any, keys: string[]) {
return keys.map((key) => person[key])
}
console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]
在上述例子中,可以看到 getValues(persion, ['gender'])打印出来的是[undefined],但是 ts 编译器并没有给出报错信息,那么如何使用 ts 对这种模式进行类型约束呢?这里就要用到了索引类型,改造一下 getValues 函数,通过 索引类型查询和 索引访问 操作符:
function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
return keys.map((key) => person[key])
}
interface Person {
name: string
age: number
}
const person: Person = {
name: 'musion',
age: 35,
}
getValues(person, ['name']) // ['musion']
getValues(person, ['gender']) // 报错:
// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
// Type "gender" is not assignable to type "name" | "age".
映射类型
根据旧的类型创建出新的类型, 我们称之为映射类型,比如我们定义一个接口。
interface TestInterface {
name: string
age: number
}
我们把上面定义的接口里面的属性全部变成可选
// 我们可以通过+/-来指定添加还是删除
type OptionalTestInterface<T> = {
[p in keyof T]+?: T[p]
}
type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
// name?:string,
// age?:number
// }
比如我们再加上只读
type OptionalTestInterface<T> = {
+readonly [p in keyof T]+?: T[p]
}
type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
// readonly name?:string,
// readonly age?:number
// }
Partial
Partial<T>
将类型的属性变成可选
type Partial<T> = {
[P in keyof T]?: T[P]
}
在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值的类。中间的 ? 号,用于将所有属性变为可选。
举例
interface UserInfo {
id: string
name: string
}
// error:Property 'id' is missing in type '{ name: string; }' but required in type 'UserInfo'
const xiaoming: UserInfo = {
name: 'xiaoming',
}
使用Partial<T>
type NewUserInfo = Partial<UserInfo>
const xiaoming: NewUserInfo = {
name: 'xiaoming',
}
这个 NewUserInfo 就相当于
interface NewUserInfo {
id?: string
name?: string
}
但是 Partial<T>
有个局限性,就是只支持处理第一层的属性,如果我的接口定义是这样的
interface UserInfo {
id: string
name: string
fruits: {
appleNumber: number
orangeNumber: number
}
}
type NewUserInfo = Partial<UserInfo>
// Property 'appleNumber' is missing in type '{ orangeNumber: number; }' but required in type '{ appleNumber: number; orangeNumber: number; }'.
const xiaoming: NewUserInfo = {
name: 'xiaoming',
fruits: {
orangeNumber: 1,
},
}
可以看到,第二层以后就不会处理了,如果要处理多层,就可以自己实现
type DeepPartial<T> = {
// 如果是 object,则递归类型
[U in keyof T]?: T[U] extends object ? DeepPartial<T[U]> : T[U]
}
type PartialedWindow = DeepPartial<T> // 现在T上所有属性都变成了可选啦
Required
Required 将类型的属性变成必选
type Required<T> = {
[P in keyof T]-?: T[P]
}
其中 -? 是代表移除 ? 这个 modifier 的标识。再拓展一下,除了可以应用于 ? 这个 modifiers ,还有应用在 readonly ,比如 Readonly<T>
这个类型
type Readonly<T> = {
readonly [p in keyof T]: T[p]
}
Readonly
Readonly<T>
的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。
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,
};
Record
Record<K extends keyof any, T>
的作用是将 K 中所有的属性的值转化为 T 类型。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInfo> = {
about: { title: "about" },
contact: { title: "contact" },
home: { title: "home" },
};
ReturnType
用来得到一个函数的返回值类型
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
infer在这里用于提取函数类型的返回值类型。ReturnType<T>
只是将 infer R 从参数位置移动到返回值位置,因此此时 R 即是表示待推断的返回值类型。
type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";
Exclude
Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。
type Exclude<T, U> = T extends U ? never : T;
如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
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
Omit
Omit<T, K extends keyof any>
的作用是使用 T 类型中除了 K 类型的所有属性,来构造一个新的类型。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
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[]
Parameters
Parameters<T>
的作用是用于获得函数的参数类型组成的元组类型。
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;
type A = Parameters<() =>void>; // []
type B = Parameters<typeofArray.isArray>; // [any]
type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
type D = Parameters<typeofMath.max>; // number[]