TypeScript 类型定义实战
类型世界
原始类型
TypeScript 在语法上是 JavaScript 的超集,自然支持 JavaScript 中的数据类型。在 JavaScript 中,原始类型包括 7 种: string、number、boolean、undefined、null、bigint、symbol。
const name: string = '前端'; const birthday: number = 20220101; const isMan: boolean = true; const foo: undefined = undefined; const bar: null = null; const integer: bigint = 100n; const sym: symbol = Symbol("key");
原始字面量类型
在原始类型的基础上,我们还可以进一步的将类型缩小为字面量类型。每个字面量类型都只有一个值,也就是字面量本身。
TypeScript 中包括如下的原始字面量类型:
- boolean 字面量类型
- string 字面量类型
- number 字面量类型
- bigint 字面量类型
其中,以 string 字面量类型为基础构造出了
模板字面量类型
Template Literal Types
。
与 JavaScript 中的模板字符串有着同样的语法,但在 TypeScript 中用于类型。
type World = "world"; type Greeting = `hello ${World}`;
对象类型
对象类型除了我们上面提到的原始对象包装类,还有数组类型、元组类型、接口类型(interface)、函数类型等等,我们先来看下三种基本的对象类型:Ojbect 类型、object 类型和对象类型字面量。
Object
Object 类型主要用来描述所有对象共享的属性和方法。比如 constructor、toString()、valueOf() 等等。
如下代码是 Object 类型的接口定义:
interface Object { constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: PropertyKey): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: PropertyKey): boolean; }
除了 undefiend 和 null 以外,其他的值都可以赋值给 Object 类型。Object 类型无法表示非原始值的类型,所以引出了下文的 object 类型。
object
TypeScript 2.2 中新增,表示非原始值的类型。
需要注意的是,不允许读取和修改 object 类型上的自定义属性,只允许访问对象公共的属性和方法。
const person: object = { name: 'xiaoming', age: 26 } person.name; // ts2339 Property 'name' does not exist on type 'object'. person.name = 'mingming'; // ts2339 Property 'name' does not exist on type 'object'. person.toString(); // ok
原始类型值不允许赋值给 object 类型,只有非原始类型的值才可以赋值给 object 类型。
let objectType: object; objectType = 'foo'; // ts2322 objectType = 1; // ts2322 objectType = true; // ts2322 objectType = 100n; // ts2322 objectType = Symbol(); // ts2322 objectType = undefined; // ts2322 objectType = null; // ts2322 objectType = {}; // ok objectType = []; // ok objectType = function() {}; // ok
object 类型只能赋值给以下三种类型:
- any 类型和 unknown 类型
- Object 类型
- 空对象类型字面量 {}
const objectType: object = {}; const anyType: any = objectType; // ok const unknownType: unknown = objectType; // ok const objType: Object = objectType; // ok const obj: {} = objectType; // ok
应用
我们知道在 JavaScript 中,Object.create() 方法的第一个参数,必须传入对象或者 null 作为新创建对象的原型。如果传入原始类型的值,那么将会产生运行时的类型错误。在没有引入 object 类型之前,没有办法描述 Object.create() 方法的签名,只能用 any。引入了 object 类型之后,就可以用它来描述了。
对象类型字面量
对象类型字面量是定义对象类型的方法之一。在操作对象类型字面量类型的值时,如果访问了未定义的属性,会产生编译错误。
const person: { name: string, age: number } = { name: 'tong', age: 26 } person.height // ts2339 Property 'height' does not exist on type '{ name: string; age: number; }'
空对象类型字面量 {}
{} 空对象类型字面量就是没有定义任何类型成员的对象类型字面量,自然不允许访问 {} 类型上的任何自定义属性。
{} 和 Object 类型很相似。从行为上来看,二者是完全可以替换使用的。
- 除了 undefined 和 null,其他任意类型的值都可以赋值给它们
- 二者之间也可以相互赋值
type T0 = {} extends Object ? true : false // true type T1 = Object extends {} ? true : false // true
二者主要的区别在语义上,Object 类型用来描述对象公共的属性和方法,不该将其用于自定义对象类型,即使不会产生编译错误。
而 {} 空对象类型字面量描述的是不包含任何属性的对象类型。
const person: Object = { name: 'tong', age: 26 }
为了更好地区分
Object
、
object
以及
{}
这三个具有迷惑性的类型,做下总结:
- 在任何时候都 不要,不要,不要使用 Object 以及类似的装箱类型。
-
当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用 object。但我更推荐进一步区分,也就是使用
Record<string, unknown>
或Record<string, any>
表示对象,unknown[]
或any[]
表示数组,(...args: any[]) => any
表示函数这样。 -
要避免使用
{}
。{}
意味着任何非null / undefined
的值,从这个层面上看,使用它和使用any
一样恶劣。
数组类型
TypeScript 中有两种定义数组的方法,如下代码所示:
// 第一种方式 const arr1: number[] = [1, 2, 3]; const arr2: string[] = ['1', '2', '3']; const arr3: (string | number)[] = [1, '2']; // 第二种方式 const arr4: Array<number> = [1, 2, 3]; const arr5: Array<string> = ['1', '2', '3']; const arr6: Array<string | number> = [1, '2'];
推荐使用第一种方式定义数组,第二种通过泛型定义的方式会与 JSX 的语法产生冲突。
元组类型
元组类型不光能够限制元素的个数,也可以限制元素的类型。
const tuple1: [number, string] = [1, '1']; const tuple2: [boolean, string] = [true, '1'];
函数类型
在 TypeScript 中,定义函数的语法如下代码所示,相比 JavaScript 的函数定义,多了一些类型标注。
function add1(x: number, y: number): number { return x + y } const add2 = function(x: number, y: number): number { return x + y } const add3 = (x: number, y: number): number => { return x + y }
使用 Type 和 Interface 定义函数
TypeScript 同样支持使用 Type 和 Interface 来定义函数签名,其中,Type 是类型别名,Interface 是接口。
type add = (a: number, b: number) => number interface Computed { add(a: number, b: number): number multi: (a: number, b: number) => number }
注意点:
- 使用 type 定义函数签名时,箭头函数与 ES6 中的箭头函数不同,这里的箭头函数右侧代表函数的返回值类型。而 ES6 中箭头函数后面跟着的是函数体。
泛型
如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数。
泛型程序设计是一种编程风格或编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。
通过泛型,我们能够定义通用的数据结构或类型,这些数据结构或类型仅在它们操作的实际类型上有差别。
泛型程序设计是实现可重用组件的一种手段。
function identity(arg: number): number { return arg; } identity(0);
此例中,identity 函数的使用场景非常有限,它只能接受 number 类型的参数。
如果使用泛型,identity 函数不但能够接受任意类型的参数,还能够保证参数类型与返回值类型是一致的。
function identity<T>(arg: T): T { return arg; }
顶端类型
在类型系统中,所有类型都是顶端类型的子类型。
TypeScript 中有两种顶端类型:
- any
- unknown
为什么要搞出两个顶端类型呢?因为 unknown 比 any 更加安全。
在 TypeScript 中,所有类型都是 any 的子类型,也就是说你可以将任何类型的值赋值给 any 类型。TypeScript 也允许将 any 类型赋值给任何其他的类型,除了 never 类型。如下代码所示:
let foo: any; foo = null; foo = undefined; foo = 1; foo = {};
any 类型相当于 TypeScript 给我们开发者留的后门,我们可以对 any 类型的值进行任何操作,哪怕是事实上并不存在的属性或方法,并且它还会使类型检查失效。
如下代码所示:
let foo: any = {} let bar = anything.bar foo.eat(); foo.run();
这无疑为代码的运行时埋下极大隐患,是我们绝对不能接受的。
所以 TypeScript 3.0 引入了 unknown 类型,unknown 也是顶端类型,任何其他类型的值都可以赋值给 unknown 类型。但是,unknown 类型只能赋值给 any 类型和 unknown 类型,这一点比 any 类型更加安全。而且在使用 unknown 类型时,我们需要将它细化为某种具体的类型,否则编译器会报错。如下代码所示:
let a: unknown = 1 let b = a + 2 // Error: Object is of type 'unknown' if (typeof a === 'number') { let c = a + 2 }
底端类型
在 TypeScript 中,底端类型只有 never 一个类型,底端类型顾名思义,是类型系统中的最底层,是所有其他类型的子类型,因此 never 类型可以赋值给任何类型。
除了 never 类型自己,所有其他的类型都不能赋值给 never
,即使是类型约束最宽松的 any 也能赋值给 never。
let a: never; a = 'foo' // ts(2322) a = 1 // ts(2322) let b: string = a let c: number = a
当 TS 发现已经没有可以用的类型时,会将类型推断为 never。如下代码所示:
function getData(data: string) { if (typeof data === 'string') { console.log(data); // string } else { console.log(data); // never } }
应用
函数返回值
当函数报错或者存在死循环时,这个时候函数无法返回一个值,可以用 never 作为函数的返回值类型。后面我们有专门的章节讲解函数,这里先做一个了解。
function fn1(): never { throw new Error('404'); } function fn2(): never { while (true) { console.log('500') } }
辅助类型运算
在一些工具类型中,never 可以辅助类型运算。
type Exclude<T, U> = T extends U ? never : T;
联合类型
联合类型表示由输入类型组成的一个更广泛、更包容的类型。TypeScript 中使用 "|" 操作符表示联合类型,我们可以将其类比成 JavaScript 中的逻辑或 “||”。
type strOrNum = string | number // string | number
type ICat = { name: string; } type IAnimal = { age: number; } type IPet = ICat | IAnimal const pet1: IPet = { name: 'mimi', } // OK const pet2: IPet = { age: 5 } // OK const pet3: IPet = { name: 'mimi', age: 5 } // OK
交叉类型
交叉类型表示由输入类型组成的一个更小,更有限制的类型。TypeScript 中使用 "&" 操作符表示联合类型,我们可以将其类比成 JavaScript 中的逻辑与 “&&”。
type neverType = string & number // never
type ICat = { name: string; } type IAnimal = { age: number; } type IPet = ICat & IAnimal const pet1: IPet = { name: 'mimi', } // err const pet2: IPet = { age: 5 } // err const pet3: IPet = { name: 'mimi', age: 5 } // OK
React & TS 最佳实践
前置内容
keyof
索引查询
keyof T
的结果为
T
上所有共有属性 key 的联合:
interface Eg1{ name: string; readonly age: number; } type T1 = keyof Eg1; // 'name' | 'age'
T[K]
索引访问
T[keyof T]
获取
T
所有
key
的类型组成的联合类型
interface Eg1{ name: string; readonly age: number; } type V3 = Eg1[keyof Eg1] // string | number
in
映射类型
in
操作符可以理解为
for...in
/
for...of
,循环遍历键名
type Clone<T> = { [K in keyof T]: T[K]; };
类型体操
模式匹配做提取
Typescript 类型的模式匹配是通过 extends 对类型参数做匹配,结果保存到通过 infer 声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。
// 提取 value 的类型 type GetValueType<P> = P extends Promise<infer Value> ? Value : never; type GetValueResult = GetValueType<Promise<'name'>> // "name"
通过 extends 对传入的类型参数 P 做模式匹配,其中值的类型是需要提取的,通过 infer 声明一个局部变量 Value 来保存,如果匹配,就返回匹配到的 Value,否则就返回 never 代表没匹配到。
重新构造做变换
类型是不可变的,当我们需要基于已有类型产生新类型时需要重新构造做变换。
修改 Value:
type Mapping<Obj extends object> = { [Key in keyof Obj]: [Obj[Key], Obj[Key], Obj[Key]] } type res = Mapping<{a: 1, b: 2}>; // type res = { a: [1, 1, 1]; b: [2, 2, 2] };
修改 Key,使用 as,这叫做
重映射
:
// 把索引类型的 Key 变为大写 type UppercaseKey<Obj extends object> = { [Key in keyof Obj as Uppercase<Key & string>]: Obj[Key] } type UppercaseKeyResult = UppercaseKey<{ good: 1 }> // type UppercaseKeyResult = { GOOD: 1 }
递归复用做循环
TypeScript 类型系统不支持循环,但支持递归。当处理数量(个数、长度、层数)不固定的类型的时候,可以只处理一个类型,然后递归的调用自身处理下一个类型,直到结束条件也就是所有的类型都处理完,就完成不确定数量的类型编程,达到循环的效果。
// 实现从长度不固定的数组中查找某个元素 type Includes<Arr extends unknown[], FindItem> = Arr extends [infer First, ...infer Rest] ? IsEqual<First, FindItem> extends true ? true : Includes<Rest, FindItem> : false; // 判断相等就是 A 是 B 的子类型并且 B 也是 A 的子类型 type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
类型参数: Arr 是待查找的数组类型,元素类型任意,也就是 unknown;FindItem 待查找的元素类型。
思路:
- 每次提取一个元素到 infer 声明的局部变量 First 中,剩余的放到局部变量 Rest。
- 判断 First 是否是要查找的元素,也就是和 FindItem 相等,是的话就返回 true,否则继续递归判断下一个元素。
- 直到结束条件也就是提取不出下一个元素,这时返回 false。
数组长度做计数
TypeScript 类型系统中没有加减乘除运算符,但是可以通过构造不同的数组然后取 length 的方式来完成数值计算,把数值的加减乘除转化为对数组的提取和构造。(严格来说是元组)
加法的实现很容易想到:
构造两个数组,然后合并成一个,取 length。比如 3 + 2,就是构造一个长度为 3 的数组类型,再构造一个长度为 2 的数组类型,然后合并成一个数组,取 length。
type BuildArray< Length extends number, Ele = unknown, Arr extends unknown[] = [] > = Arr['length'] extends Length ? Arr : BuildArray<Length, Ele, [...Arr, Ele]>; type BuildArrayResult = BuildArray<5> // type BuildArrayResult = [unknown, unknown, unknown, unknown, unknown]
- 类型参数 Length 是要构造的数组的长度。
- 类型参数 Ele 是数组中元素的类型,默认为 unknown。
- 类型参数 Arr 为构造出的数组,默认是 []。
如果 Arr 的长度到达了 Length,就返回构造出的 Arr,否则继续递归构造。
构造数组实现了,那么基于它就能实现加法:
type Add<Num1 extends number, Num2 extends number> = [...BuildArray<Num1>, ...BuildArray<Num2>]['length']; type AddResult = Add<32, 25>; // 57
联合分散可简化
当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型。
比如这样一个联合类型:
type Union = 'a' | 'b' | 'c'; // 我们想把其中的 a 大写,就可以这样写: type UppercaseA<Item extends string> = Item extends 'a' ? Uppercase<Item> : Item; type result = UppercaseA<Union> // type result = "b" | "c" | "A"
这样确实是简化了类型编程逻辑的,不需要递归提取每个元素再处理。
注意:
- A extends B 才是分布式条件类型, [A] extends [B] 就不是了,只有左边是单独的类型参数才可以。
- [A] extends [B] 这样不直接写就可以避免触发分布式条件类型,那么 B 就是整个联合类型。
如下例:
type Union = 'a' | 'b' | 'c'; type UppercaseA<Item extends string> = [Item] extends ['a'] ? Uppercase<Item> : Item; type result = UppercaseA<Union> // type result = "a" | "b" | "c";
特殊特性要记清
学会了
提取、构造、递归、数组长度计数、联合类型分散
后,各种类型体操都能写,但是还需要记一些特殊类型的特性。
比如 any 和任何类型的交叉都为 any,可以用来判断 any 类型:
type IsAny<T> = 'dong' extends ('guang' & T) ? true : false
比如索引一般是 string,而可索引签名不是,可以根据这个来过滤掉可索引签名:
type RemoveIndexSignature<Obj extends Record<string, any>> = { [ Key in keyof Obj as Key extends `${infer Str}`? Str : never ]: Obj[Key] }
TypeScript 内置的高级类型
模式匹配
Parameters
Parameters 用于提取函数类型的参数类型,将参数类型放在一个元组中。
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; type Eg = Parameters<(arg1: string, arg2: number) => void>; // [arg1: string, arg2: number]
类型参数 T 为待处理的类型,通过 extends 约束为函数,参数和返回值任意。
通过 extends 判断
T
是否是函数类型,如果是则使用
inter P
提取参数的类型,并将结果存到类型
P
上,否则就返回
never
。
ReturnType
ReturnType 用于提取函数类型的返回值类型,和 Parameters 基本一样,只是使用 infer R 的位置不一样。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
ConstructorParameters
ConstructorParameters 用于获取类的构造函数的参数类型,存在一个元组中。
type ConstructorParameters< T extends abstract new (...args: any) => any > = T extends abstract new (...args: infer P) => any ? P : never; interface ErrorConstructor { new(message?: string): Error; (message?: string): Error; readonly prototype: Error; } type Eg = ConstructorParameters<ErrorConstructor>; //string
类型参数 T 是待处理的类型,通过 extends 约束为构造器类型,加个 abstract 代表不能直接被实例化(不加也行)。
判断
T
是满足约束的类时,利用
infer P
提取构造函数的参数类型到局部变量 P 里,返回 P。
InstanceType
InstanceType 用于获取构造函数的返回值类型,和 ConstructorParameters 基本一样,只是使用 infer R 的位置不一样。
type InstanceType< T extends abstract new (...args: any) => any > = T extends abstract new (...args: any) => infer R ? R : any;
重新构造
Partial
Partial<T>
将
T
的所有属性变成可选的。
type Partial<T> = { [P in keyof T]?: T[P]; };
[P in keyof T]
通过映射类型,遍历
T
上的所有属性,将每个属性设置为可选属性。
Required
Required<T>
将
T
的所有属性变成必选的,和 Partial 基本一样,只是由可选改为去掉可选,也就是 -?。
type Required<T> = { [P in keyof T]-?: T[P]; };
Readonly
Readonly<T>
将
T
的所有属性变成只读的,和 Partial 基本一样,只是加上 readonly 修饰符。
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
Pick
Pick<T, K>
用于从
T
中选出
K
并组成一个新的类型。
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; type Person = { name: string; age: number; email: string; }; type Result = Pick<Person, 'name' | 'email'>; // {name: string;email: string;}
类型参数 T 为待处理的类型,类型参数 K 为要过滤出的索引,通过 extends 约束 K 为 T 的索引的子集。
构造新的索引类型返回,索引取自
K
,也就是
P in K
,值则是它对应的原来的值,也就
T[P]
。
Record
Record<K, T>
用于构造一个
type
,
key
为联合类型中的每个子类型,类型为
T
。
type Record<K extends keyof any, T> = { [P in K]: T; }; type Eg1 = Record<'a' | 'b', {key1: string}> // {a: { key1: string; };b: { key1: string; };}
思路:遍历
K
,将值设置为
T
。需要注意的是
keyof any
得到的是
string | number | symbol
,因为key的类型只能为
string | number | symbol
。
分布式条件类型
Exclude
Exclude<T, U>
提取存在于
T
,但不存在于
U
的类型组成的联合类型。
type Exclude<T, U> = T extends U ? never : T; type Eg = Exclude<'key1' | 'key2', 'key2'> // 'key1'
遍历T中的所有子类型,如果该子类型约束于U(存在于U、兼容于U),则返回never类型,否则返回该子类型。
Extract
Extract<T, U>
提取
T
和
U
的所有交集。
Exclude 反过来就是 Extract,也就是取交集。
type Extract<T, U> = T extends U ? T : never; type Eg = Extract<'key1' | 'key2', 'key1'> // 'key1'
Omit
Omit<T, K>
从类型
T
中剔除
K
中的所有属性。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
可以利用
Pick
提取我们需要的 keys 组成的类型,即
Omit = Pick<T, 我们需要的属性联合>
,而我们需要的属性联合就是 T 的属性联合中排除存在于 K 中的类型,即
Exclude<keyof T, K>
。
如果不利用Pick实现呢?
type Omit2<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P] }
其实现类似于
Pick
,区别在于是遍历的属性不一样,我们需要的属性和上面的例子一样,就是
Exclude<keyof T, K>
。
Ts compiler 内部实现的类型
- Uppercase 用于将字符串转大写。
- Lowercase 用于将字符串转小写。
- Capitalize 将字符串首字母大写。
- Uncapitalize 去掉字符串首字母大写。
这些类型工具,在
lib.es5.d.ts
文件中是看不到具体定义的,这是因为这部分类型不是在 ts 里实现的,而是编译过程中由 js 实现的,我们可以在源码里找到对应的处理代码。
lib.es5.d.ts
:
type Uppercase<S extends string> = intrinsic; type Lowercase<S extends string> = intrinsic; type Capitalize<S extends string> = intrinsic; type Uncapitalize<S extends string> = intrinsic;
源码:
开源库中的 Ts 高级类型
https://
github.com/piotrwitek/u
tility-types
SymmetricDifference
SymmetricDifference<T, U>
获取没有同时存在于T和U内的类型。
源码:
/** * SetDifference (same as Exclude) * @desc Set difference of given union types `A` and `B` * @example * // Expect: "1" * SetDifference<'1' | '2' | '3', '2' | '3' | '4'>; * * // Expect: string | number * SetDifference<string | number | (() => void), Function>; */ export type SetDifference<A, B> = A extends B ? never : A; /** * SymmetricDifference * @desc Set difference of union and intersection of given union types `A` and `B` * @example * // Expect: "1" | "4" * SymmetricDifference<'1' | '2' | '3', '2' | '3' | '4'>; */ export type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;
思路:SetDifference 就是上文实现的 Exclude,先提取存在于
A
但不存在于
A & B
的类型,然后再提取存在于
B
但不存在于
A & B
的,最后进行联合。
-
A | B
:利用联合类型在 extends 中的分发特性,可以理解为SetDifference<A, A & B> | SetDifference<B, A & B>
-
A & B
:获取所有类型的交叉类型
PickByValueExact
PickByValueExact<T, V>
提取指定值的类型,即从一个类型
T
中选出那些属性的值类型精确匹配
ValueType
的属性,并返回一个新类型。
源码:
/** * PickByValueExact * @desc From `T` pick a set of properties by value matching exact `ValueType`. * @example * type Props = { req: number; reqUndef: number | undefined; opt?: string; }; * * // Expect: { req: number } * type Props = PickByValueExact<Props, number>; * // Expect: { reqUndef: number | undefined; } * type Props = PickByValueExact<Props, number | undefined>; */ export type PickByValueExact<T, ValueType> = Pick< T, { [Key in keyof T]-?: [ValueType] extends [T[Key]] ? [T[Key]] extends [ValueType] ? Key : never : never; }[keyof T] >;
思路:
- Pick 用来从一个类型中选出某些属性并组成一个新的类型。
-
对
T
中的每个属性进行遍历,判断每个属性的值类型是否与ValueType
相等。如果相等,就将这个属性名称返回,否则就返回 never。 - 将所有返回的属性名称合并成一个新对象,并且排除掉原对象中没有被选中的属性。
FunctionKeys
FunctionKeys<T>
获取
T
中所有函数属性的名称。
/** * NonUndefined * @desc Exclude undefined from set `A` * @example * // Expect: "string | null" * SymmetricDifference<string | null | undefined>; */ export type NonUndefined<A> = A extends undefined ? never : A; /** * FunctionKeys * @desc Get union type of keys that are functions in object type `T` * @example * type MixedProps = {name: string; setName: (name: string) => void; someKeys?: string; someFn?: (...args: any) => any;}; * * // Expect: "setName | someFn" * type Keys = FunctionKeys<MixedProps>; */ export type FunctionKeys<T extends object> = { [K in keyof T]-?: NonUndefined<T[K]> extends Function ? K : never; }[keyof T];
思路:约束参数T类型为
object
,通过映射类型
K in keyof T
遍历所有的key,判断
T[K]
是否为函数类型,是的话返回
K
,否则
never
。
MutableKeys
MutableKeys<T>
获取
T
所有非只读(可变)类型的 key 组成的联合类型。
/** * @desc 一个辅助类型,判断X和Y是否类型相同, * @returns 是则返回A,否则返回B */ type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends < T >() => T extends Y ? 1 : 2 ? A : B; /** * MutableKeys * @desc Get union type of keys that are mutable in object type `T` * Credit: Matt McCutchen *
https://
stackoverflow.com/quest
ions/52443276/how-to-exclude-getter-only-properties-from-type-in-typescript
* @example * type Props = { readonly foo: string; bar: number }; * * // Expect: "bar" * type Keys = MutableKeys<Props>; */ export type MutableKeys<T extends object> = { [P in keyof T]-?: IfEquals< { [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P >; }[keyof T];
思路:
-
[P in keyof T]-?: IfEquals<...>
表示将 T 的每个属性都设置为必选属性,并且通过IfEquals
来判断该属性是否是可变的。 -
-readonly [Q in P]: T[P]
表示将 T 中属性 P 的只读属性去掉,变成可写的属性 -
如果类型
{ [Q in P]: T[P] }
和{ -readonly [Q in P]: T[P] }
相等,则说明属性P
是可变的,因为它的只读属性被定义为可写。在这种情况下,IfEquals
类型的结果为P
,否则为never
。
参考内容:
TypeScript 5.0 正式发布:
https://
zhuanlan.zhihu.com/p/61
5501751
TypeScript官网:
https://www.
typescriptlang.org/docs
/handbook/utility-types.html
更多内置高级类型:
https://
github.com/ascoders/wee
kly/tree/master/TS%20
https://
github.com/total-typesc
ript/advanced-typescript-workshop/tree/main/exercises
https://
zhuanlan.zhihu.com/p/61
4782362