一文搞懂typescript工具类型及其原理
前言
相信很多人都在学习typescript,这里介绍泛型常见的工具类型函数(Utility Types)
及其在typescript中的源码实现。
工具函数
可能有些刚开始学ts的还不清楚什么工具类型指的是什么,工具类型就是TypeScript提供的一些实用类型来实现常见的类型转换,这些类型是全局可用的,所有的类型定义在typescript源码中也都能找到。
Partial
Partial<T>,将传入T类型的所有属性设为可选属性。
例如下面的实例代码,定义一个user接口类型,通过Partial,生成一个所有属性为可选的新的user类型。一个常见的使用场景就是合并或更新对象属性的方法,还有组件写props时也非常好用。
interface User {
name: string;
age: num;
}
type UpdateUser = Partial<User>
// {name?: string; age?: number}
原理:首先通过keyof T
,遍历出类型T
的所有属性,然后通过in
操作符进行遍历,最后在属性后加上?
,将属性变为可选属性。
// https://github.com/microsoft/TypeScript/blob/HEAD/src/lib/es5.d.ts#L1517
type Partial<T> = {
[P in keyof T]?: T[P];
}
Required
Required,可以将传入参数的所有属性设为必选属性。
与Partial相反的一个方法,通过Required函数声明的类型属性会全部变为必选,看示例代码。
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
const obj2: Required<Props> = { a: 5 }; // Property 'b' is missing in type '{ a: number; }'
原理:用于将 T
类型的所有属性设置为必选状态,首先通过 keyof T
,取出类型 T
的所有属性, 然后通过 in
操作符进行遍历,最后在属性后的 ?
前加上 -
,将属性变为必选属性。
这里顺便讲下-
这个符号的作用,这是TypeScript 2.8为映射类型增加了添加或删除特定修饰符的能力。具体来说,readonly
和?
映射类型中的属性修饰符现在可以加上+
或-
前缀,以指示应该添加或删除该修饰符,当然一般+
号是可以省略的。详情有2.8版本的官方文档里有提到(点击此链接跳转到官网。
type Required<T> = {
[P in keyof T]-?: T[P];
}
Readonly
Readonly<T>
将T
的所有属性映射为只读的,相当于在所有属性上加了readonly。加上readonly的属性即为只读属性,不可修改。
interface IPerson {
name: string
age: number
}
type IReadOnlyPerson = Readonly<IPerson>
/**{
readonly name: string;
readonly age: number;
}*/
原理: 遍历T
的所有属性并加上readonly
type Readonly<T> = { readonly [P in keyof T]: T[P] }
Record
Record<T, K>
,接收两个泛型,生成key为K类型,value为T类型的对象类型。
示例代码:
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
原理:接收两个泛型,K
为string | number | symbol
可以继承的类型,这三个也是对象key所支持的基础类型,然后通过 in
操作符对 K
进行遍历,每一个属性的类型为T
类型。
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
}
Pick
Pick<T, K>
,将T
类型中的K
属性提取出来,生成新的类型,类型过滤的意思。类似于与lodash中的pick方法。
示例:定义一个矩形Rectangular类型,包含长宽高属性,而如果是正方形的时候,只需要一个长度的属性,这时就可以用Pick
方法提取Rectangular中的length属性生成正方形的类型。
interface Rectangular {
length: number
height: number
width: number
}
type Square = Pick<Rectangular, 'length'>
// { length: number; }
const temp: Square = { length: 5 }
原理:从 T
类型中提取部分属性,作为新的返回类型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
Omit
Omit<T, K>
,与Pick相反,将T
类型中的K
属性全部删除,生成新的类型。
示例:同Pick示例,反过来用Omit
删除正方形所不需要Rectangular的其他属性,生成正方形的类型。
interface Rectangular {
length: number
height: number
width: number
}
type Square = Omit<Rectangular, 'height' | 'width'>
// 返回类型:
type Square = {
length: number;
}
const temp: Square = { length: 5 }
原理:结合Pick
和Exclude
方法,提取出不含K
属性的类型。
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
Exclude
Exclude<T, U>
,从联合类型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>;
原理:源码很简单,判断联合类型T是否可以赋值给联合类型U,是则返回T,否则返回never。never是一个特殊的类型,在这里可以表示为空的联合类型,在于与其他类型的联合后,结果为其他类型。
type Exclude<T, U> = T extends U ? never : T;
这里还涉及到extends条件类型的特殊情况,extends的前参T如果是一个泛型参数。对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,两个类型会成为分配条件类型(Distributive Conditional Types)。分配条件类型是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。
大家可能看了还是一脸懵,啥分配条件类型呀,看我举个🌰:
type P<T> = T extends 'x' ? string : number;
type A = P<'x' | 'y'> // A的类型为string | number
该例中,extends的前参为T,T是一个泛型参数。在A的定义中,给T传入的是联合类型'x' | 'y',满足分配律,将'x'和'y'分别带入P<T>
P<'x' | 'y'> => P<'x'> | P<'y'>
'x'代入得到
'x' extends 'x' ? string : number => string
'y'代入得到
'y' extends 'x' ? string : number => number
然后将每一项代入得到的结果联合起来,得到string | number
总之,满足两个要点即可适用分配律:第一,参数是泛型类型,第二,代入参数的是联合类型
Extract
Extract<T, U>
,提取T
中可以赋值给U
的部分属性,即提取两边相交的属性,也就是并集的概念。
示例:
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T02 = Extract<string | number | (() => void), Function>; // () => void
原理:与Exclude相反,判断联合类型T是否可以赋值给联合类型U,是则返回never,否则返回T。
type Extract<T, U> = T extends U ? T : never;
NonNullable
NonNullable<T>
,过滤类型中的 null 及 undefined 类型。
示例:
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
原理:判断T是否可以赋值给null或者undefined类型,是则返回never,否则返回T,如果这段看不明白的可以再看下Exclude那段关于extends的补充说明。
type NonNullable<T> = T extends null | undefined ? never : T
Parameters
Parameters<T>
,T
为函数类型,获取函数的参数类型组成 元组
示例:如果传入的不是函数类型的将会报错
declare function f1(arg: { a: number; b: string }): void;
type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [s:string]
type T2 = Parameters<<T>(arg: T) => T>; // [arg: unknown]
type T3 = Parameters<any>; // unknown[]
type T3 = Parameters<never>; // never
原理:
Parameters
首先约束参数T
必须是个函数类型- 判断
T
是否是函数类型,如果是则使用infer P
暂时存一下函数的参数类型,直接用 P 即可得到这个类型并返回,否则就返回never
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
这里用到了infer,infer P
标记一个泛型,表示这个泛型是一个待推断的类型,并且可以直接使用。
ReturnType
ReturnType<T>
,获取函数的返回值类型
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
原理:与Parameters类似
ReturnType
首先约束参数T
必须是个函数类型- 判断
T
是否是函数类型,如果是则使用infer R
暂时存一下函数的返回值类型,后面的语句直接用 R 即可得到这个类型并返回,否则就返回any
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
总结
类型 | 用法 |
---|---|
Partial<T> | 将T 中所有的属性都变成可选类型 |
Required<T> | 将T 中所有的属性都变成必选类型 |
Readonly<T> | 将T所有属性转为只读 |
Record<T, K> | 返回属性名为类型K ,值为T 的对象类型 |
Pick<T, K> | 将T 类型中的K 属性提取出来 |
Omit<T, K> | 将T 类型中的K 属性剔除 |
Exclude<T, U> | 从联合类型T 中剔除可以赋值给类型U 的部分 |
Extract<T, U> | 提取联合类型T 和联合类型U 交集部分 |
NonNullable | 过滤类型中的 null 及 undefined 类型 |
Parameters | 获取函数的参数类型组成 元组 |
ReturnType | 获取函数的返回类型 |
参考链接
最后
文中有些写的不是很准确或者说得不对的,欢迎指出来,十分感谢。
转载自:https://juejin.cn/post/7098122956542312484