外观
04.类型编程
8304字约28分钟
2024-05-31
索引签名(映射)类型
type User = {
name: string;
age?: number;
sex?: string;
};
前面的代码中,我们可以通过修饰符?
限定有哪些属性值,但是最多也就是 name,age 和 sex 这三个属性,无非也就是 age 和 sex 这两个属性写与不写的问题了。
如果希望在 Typescript 中也能动态的添加属性,还是不行,这个时候我们可以借助索引签名类型(Index Signatures)
type User = {
[key: string]: string;
};
const user: User = {
name: 'hayes',
sex: '男',
};
[key:T]:U
这种写法称为索引签名,相当于通过这种简单的方式告诉 Typescript,指定的对象可能有更多的键。基本的意思是:“在这个对象中,类型为 T 的键,对应的值为 U 类型”
在这个例子中我们声明的键的类型为 string([key: string]
),这也意味着在实现这个类型结构的变量中只能声明字符串类型的键
但由于 JavaScript 中,对于 user[prop]
形式的访问会将数字索引访问转换为字符串索引访问,也就是说, user[123]
和 user['123']
的效果是一致的。因此,在字符串索引签名类型中我们仍然可以声明数字类型的键。类似的,symbol 类型也是如此:
const user: User = {
name: 'hayes',
sex: '男',
123: '123',
[Symbol('a')]: 'symbol',
};
索引签名类型也可以和具体的键值对类型声明并存,但是需要注意,具体的键值类型也需要符合索引签名类型的声明:
type User = {
[key: string]: string;
name: string;
// age:number //error
};
如果希望这里的 age 不报错,上面的索引签名类型可以使用联合类型
type User = {
[key: string]: string | number | symbol | undefined;
name: string;
age: number;
};
索引签名类型最常见场景是在重构 JavaScript 代码的时候或者创建类型声明的时候,为内部属性较多的对象声明一个 any 的索引签名类型,以此来暂时支持对类型未明确属性的访问
type AnyTypeHere = {
[key: string]: any;
};
而且,之前我们必须声明属性明确的对象字面量类型,这对于有些时候声明一个空的对象就不太友好,但是又不能直接声明对象为 obj,那么这里的索引签名类型就非常合适这个场景了。
type AnyTypeHere = {
[key: string]: any;
};
let obj: AnyTypeHere = {
name: 'jack',
age: 13,
};
其实,Typescript 也专门提供了一个类似的工具类型Record,方便这种情况我们的使用
keyof
keyof
keyof
操作符。它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型。
type User = {
id: number;
name: string;
age: number;
};
type UserKeys = keyof User; // keyof User = 'id' | 'name' | 'age'
在 VS Code 中悬浮鼠标只能看到 keyof User
,看不到其中的实际值,你可以这么做
type UserKeys = keyof User & {}; // "id" | "name" | "age"
甚至我们可以结合这typeof
,直接从一个对象上,获取这个对象键的所有联合类型
const user = {
id: 1,
name: 'hayes',
age: 19,
};
type UserKeys = keyof typeof user; // "id" | "name" | "age"
也可以和方括号运算符结合
type Person = {
age: number;
name: string;
sex: boolean;
};
// number|string|boolean
type A = Person[keyof Person];
结合着泛型,方括号运算符以及 extends 受限的泛型,可以直接重写之前我们在重载中写过的代码:
type TagName = keyof HTMLElementTagNameMap;
function createElement<T extends TagName>(tag: T): HTMLElementTagNameMap[T] {
return document.createElement(tag);
}
const a = createElement('a'); // ok
in
运算符遍历
in
运算符遍历
前面讲了 in 运算符在 Typescript 可以用来检查属性,在控制流中实现对类型的守卫。
除了类型守卫的作用,in
运算符还能遍历联合类型的每一个成员类型
type U = 'a' | 'b' | 'c';
type Foo = {
[key in U]: string;
};
// 等同于
// type Foo = {
// a: string,
// b: string,
// c: string
// };
上面的讲解keyof
的时候,不是用到了这样的写法
type A = keyof Person;
那完全可以把keyof
放入到索引中使用
type User = {
readonly id: number;
name: string;
tel: string;
address?: string;
};
type CopyUser = {
[key in keyof User]: User[key];
};
const u: CopyUser = {
id: 1,
name: 'aaa',
tel: '123456',
address: 'beijing',
};
现在固定了keyof User
,那么我们可以使用泛型,增加一般性
type Copy<T> = {
[key in keyof T]: T[key];
};
const u: Copy<User> = {
id: 1,
name: 'aaa',
tel: '123456',
address: 'beijing',
};
type Animal = {
name: string;
age: number;
color: string;
type: string;
};
const dog: Copy<Animal> = {
name: 'jack',
age: 3,
color: 'black',
type: 'dog',
};
注意:
keyof T
这两个的结合得到的是一个联合类型string | number | symbol
,因为T
是泛型,并不知道T
类型的每个键到底是什么类型,可以看一下keyof any
的结果当然
[key in keyof T]
现在这样写没有什么问题,但是如果后面要和 as,和模板字符串类型连用的话,要注意类型的转换,最好直接让键就是string
类型
[key in keyof T & string]
类型编程的理解
类型编程的理解
属性修饰符
我们类型编程的代码已经逐步过渡成对泛型的处理:
type User = {
readonly id: number;
name: string;
tel: string;
address?: string;
};
type CopyUser = {
[key in keyof User]: User[key];
};
type Copy<T extends object> = {
[key in keyof T]: T[key];
};
上面的Copy<T>
类型只需要我们稍稍做修改,就能成为一个很有用的新的类型别名
type MyReadonly<T> = {
readonly [key in keyof T]: T[key];
};
就在之前的代码签名加了readonly
,这个类型别名就能实现将你传递的类型 T 所有的属性变为readonly
type User = {
readonly id: number;
name: string;
tel: string;
address?: string;
};
type MyReadonly<T> = {
readonly [key in keyof T]: T[key];
};
type ReadonlyUser = MyReadonly<User>;
const u: ReadonlyUser = {
id: 1,
name: 'jack',
tel: '135678',
address: 'beijing',
};
u.id = 2; // error 无法分配到 "id" ,因为它是只读属性
u.name = 'tom'; // error 无法分配到 "name" ,因为它是只读属性
又或者说,直接在后面加上?
,就能将原来类型中所有的属性变为可选
type MyPartial<T> = {
[key in keyof T]?: T[key];
};
type OptionalUser = MyPartial<User>;
const u: OptionalUser = {
id: 1,
name: 'jack',
};
MyReadonly
是Readonly<Type>
的具体实现
MyPartial
是Partial<Type>
的具体实现
其实,这种在现有类型的基础上创建新的类型的方式,在 TS 中也有专门的称呼:映射类型(Mapped Types),其实和索引签名类型很类似,差别只是索引签名用于定义对象可以有哪些类型的键和值,适用于属性名未知或动态的情况。映射类型则允许你在现有类型的基础上创建新的类型,通过对原始类型的属性进行转换或应用修饰符,来满足更具体的类型设计需求
修饰操作符+
,-
其实上面的readonly
与?
的写法是简写,具体应该是给原来的类型加上readonly
,给原来的类型加上?
+
修饰符:写成+?
或+readonly
,为映射属性添加?
修饰符或readonly
修饰符。
type MyReadonly<T> = {
+readonly [key in keyof T]: T[key];
};
type MyPartial<T> = {
[key in keyof T]+?: T[key];
};
既然有+
,那就有-
–
修饰符:写成-?
或-readonly
,为映射属性移除?
修饰符或readonly
修饰符。
type MyRequired<T> = {
-readonly [key in keyof T]-?: T[key];
};
const u: MyRequired<User> = {
id: 1,
name: 'jack',
tel: '135678',
address: 'beijing', // 不写现在会报错,因为已经移除了可选属性
};
u.id = 2; // ok,因为已经移除了只读属性
泛型编程的理解
Javascript 的编程大家很熟悉,如果我们想处理一个值,然后返回一个新的值,理所应当的想到的就是函数
function myPartial(type){
const newType = getOptional(type);
......
return newType
}
const type = {xxxxxx};
const newType = myPartial(type);
上面的伪代码使用函数无非就两步:
1、声明函数,传入参数
2、调用函数,获取到新的返回值
如果我们操作类型,也能像 Javascript 的函数处理一样,操作旧类型,然后得到了新的类型,那就很方便了。
// 可以当做是函数,可以接受任意类型。
// 由于是这里的 “Type” 形参,因此理什么名字都无所谓,和函数的形参名是一个意思。
type MyPartial<Type> = { todos... }
// 可以当做是函数调用,调用的时候传入了具体的类型 User
// 返回了新的类型 PartialedUser
type PartialedUser = MyPartial<User>
来看一下有多像:
声明的时候
调用的时候
最后只需要将声明中{todos...}
相关语法,换成 Typescript 的语法就行了
type MyPartial<T> = {
[key in keyof T]?: T[key];
};
有了这个理论,我们来看之前的映射类型,我们写了这样的代码:
type AnyTypeHere = {
[key: string]: any;
};
这样写肯定是有一定缺陷的,固定了键的类型,而且值的类型是 any,当然我们介绍了 Record 工具的用法,可以通过 Record 工具,帮我们定义需要的泛型。这个简单工具,我们完全也可以自己实现。
// 对于js来说,我们对值操作
function MyRecord(key,value){
// todos...
return {....}
}
// 对于TS来说,我们对类型操作
type MyRecode<K,V> = {
// todos......
}
K 我们需要限定一下类型,而V传入什么类型,就应该是什么类型。那这个不是就很简单吗?
type MyRecode<K extends string | number | symbol,V> = {
[key in K]:V
}
关联泛型
关联泛型
如果现在希望实现这么一个效果,在原有对象类型的属性上进行挑选,根据挑选属性的结果,形成新的类型
type User = {
readonly id: number;
name: string;
tel: string;
address?: string;
};
// 比如挑选name和tel属性,形成下面的类型
type UserPick = {
name: string;
tel: string;
};
type MyPick<T, K extends keyof T> = {
[key in K]: T[key];
};
type Admin = MyPick<User, 'name' | 'tel'>;
const u: Admin = {
name: 'aaa',
tel: '123456',
};
关键点在于:
1、确定需要的泛型参数个数
2、第二个泛型参数的类型应该来源于第一个参数
方括号运算符常见操作
方括号运算符常见操作
获取值的类型
type User = {
readonly id: number;
name: string;
tel: string;
address?: string;
};
// type ValueType = User['id' | 'name'];
type ValueType = User[keyof User];
// 泛型
type MyReadonly<T> = {
+readonly [key in keyof T]: T[key];
};
数组一样可以处理
const arr = ['admin', 'user', 'client'];
type ArrType = (typeof arr)[number]; // string
将上面的数组通过as const
转为只读元组类型之后,得到的是具体字面量类型的联合
const arr = ['admin', 'user', 'client'] as const;
type ArrType = (typeof arr)[number]; // "admin" | "user" | "client"
当然,我们也能写成泛型工具
type ArrType<T extends readonly any[]> = T[number];
type A = ArrType<['admin', 'user', 'client']>;
获取数组的长度
可以通过['length']
获取元组类型的具体长度 number 字面量类型,注意如果仅仅是数组,只能获取 number 类型
const arr = ['admin', 'user', 'client'] as const;
type Len = (typeof arr)['length'];
let n: Len = 3;
同样也能写成泛型工具:
type ArrLen<T extends readonly any[]> = T['length'];
type B = ArrLen<[1, 2, 3, 4, 5, 6]>; //6
结合泛型使用扩展运算符
比如现在希望写一个泛型工具,实现两个元组类型的拼接
type Result = Concat<[1, 2], [3, 4]>; //[1,2,3,4]
咋一看没啥思路,但是其实 ts 和 js 一样,支持... Spread扩展运算符
type Concat<T extends any[], U extends any[]> = [...T, ...U];
type C = Concat<[1, 2, 3, 4], ['a', 'b', 'c']>;
条件类型与类型兼容性
条件类型与类型兼容性
条件类型是 ts 中非常强大的功能,看起来有点像 JavaScript 中的条件表达式(条件 ? true 表达式 : false 表达式
):
SomeType extends OtherType ? TrueType : FalseType
当
extends
左边的类型可以赋值给右边的类型时(extends
左边的类型与右边兼容时),你将获得第一个分支(“true” 分支)中的类型;否则你将获得后一个分支(“false” 分支)中的类型。
不过首先要解惑的是,为什么使用extends
?,而不是===
或者其他运算符。
这是因为在类型层面中,对于能够进行赋值操作的两个变量,我们并不需要它们的类型完全相等,只需要具有兼容性,而两个完全相同的类型,其 extends 自然也是成立的。
type T = 1 extends number ? true : false; // true
在实际操作中,我们经常会使用条件类型来判断一个类型和另一个类型是否兼容
type T1 = 1 extends number ? true : false; // true
type T2 = '1' extends number ? true : false; // false
type T3 = string extends object ? true : false; // false
type T4 = { a: 1 } extends object ? true : false; // true
type T5 = { a: 1; b: 2 } extends { a: 1 } ? true : false; // true
type T6 = { a: 1 } extends { a: 1; b: 2 } ? true : false; // false
type T8 = string extends {} ? true : false; // true
大家可以下去自己慢慢测试类型兼容性。
但是,下面的代码会让你产生困惑:
type T9 = {} extends object ? true : false; // true
type T10 = object extends {} ? true : false; // true
type T11 = {} extends Object ? true : false; // true
type T12 = Object extends {} ? true : false; // true
type T13 = Object extends object ? true : false; // true
type T14 = object extends Object ? true : false; // true
这三个建议大家不需要细究,知道他们有这个问题:你中有我,我中有你。这是**TS“系统设定”**的问题。
记住给大家的这个图:
原始类型 < 原始类型对应的装箱类型 < Object 类型
其实还有更神奇的:
type T15 = string extends any ? true : false; // true
type T16 = Object extends any ? true : false; // true
type T17 = Object extends unknown ? true : false; // true
type T18 = any extends Object ? 1 : 2; // 1 | 2
type T19 = any extends 'Hello' ? 1 : 2; // 1 | 2
type T20 = unknown extends any ? 1 : 2; // 1
type T21 = any extends unknown ? 1 : 2; // 1
是不是很神奇?实际上,还是因为TS“系统设定”的原因,因为 any 其实从系统底层的意义来说,就是为了保证和 js 的兼容性存在的。大家不需要纠结。记住any/unknown是所有类型的顶层类型就行
别忘记,never
类型是所有类型的子类型
type T22 = never extends 'Hello' ? true : false; // true
type T23 = 'Hello' extends never ? true : false; // false
条件类型与泛型
条件类型与泛型
条件类型当然可以和泛型结合,然后组合出很多类型编程相关的处理。
我们可以定义一个泛型类型IsString
,根据T
的类型,判断返回的具体类型是true
还是false
:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'abc'>; // true
type D = IsString<123>; // false
再来有下面的题目:
实现一个 IF
类型,它接收一个条件类型 C
,一个判断为真时的返回类型 T
,判断为假时的返回类型 F
。 C
只能是 true
或者 false
, T
和 F
可以是任意类型。
type A = If<true, 'a', 'b'>; // 'a'
type B = If<false, 'a', 'b'>; // 'b'
这就非常的简单了:
type If<C extends boolean, T, F> = C extends true ? T : F;
若位于
extends
右侧的类型包含位于extends
左侧的类型(即狭窄类型 extends 宽泛类型)时,结果为 true,反之为 false。这对于基础类型和字面量类型来说大家很容易分辨。如果是对象呢?
当
extends
作用于对象时,若在对象中指定的 key 越多,则其类型定义的范围越狭窄,对象字面量的兼容性问题是我们一直提及的,希望大家注意。
上面这句话,其实我们之前在受限的泛型中已经感受过了
type ObjLength = {
length: number;
};
function getObjLength<T extends ObjLength>(obj: T) {
return obj;
}
getObjLength('Hello World');
getObjLength([1, 2, 3]);
getObjLength({ id: 1, length: 2 });
函数中传入的泛型 T 只要拥有length: number
属性,就兼容。在条件类型中同样适用
type Result = { a: string; b: boolean } extends { a: string } ? true : false; // true
extends
左边的对象字面量类型{ a: string, b: boolean }
拥有两个属性,右边的对象字面量类型{ a: string }
只有一个属性,左边有更多的属性,并且和右边有一样的属性{ a: string }
。那么我们就可以说对象字面量类型{ a: string, b: boolean }
和{ a: string }
类型兼容,因此上面的Result
的类型为true
上面的代码中不是写过这样的代码吗?
type Message<T extends { message: unknown }> = T['message'];
const person = {
id: 1,
message: 'hello',
};
type PersonMessage = Message<typeof person>;
如果没有 message 类型现在这里的代码 typescript 会提示报错,我们也能通过判断让其获取其他类型
type Message<T> = T extends { message: unknown } ? T['message'] : never;
const person = {
id: 1,
// message:"hello"
};
type PersonMessage = Message<typeof person>; // never
比如还能根据方括号运算符的特点,直接提取数组的类型
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number[]>; // number
const arr = [
{ id: 1, name: 'aaa' },
{ id: 2, name: 'bbb' },
{ id: 3, name: 'ccc' },
];
// 对象字面量类型 {id: number, name: string}
type A = Flatten<typeof arr>;
来写一个现在看起来稍微离谱的写法:
type GetType<T> = T extends string
? 'string'
: T extends number
? 'number'
: T extends bigint
? 'bigint'
: T extends boolean
? 'boolean'
: T extends symbol
? 'symbol'
: T extends undefined
? 'undefined'
: T extends null
? 'null'
: T extends any[]
? 'array'
: T extends Function
? 'function'
: 'object';
type T0 = GetType<string>; // "string"
type T1 = GetType<123n>; // "bigint"
type T2 = GetType<true>; // "boolean"
type T3 = GetType<() => void>; // "function"
type T4 = GetType<[]>; // "array"
type T5 = GetType<{}>; // "object"
type T6 = GetType<null>; // "null"
再来上点难度:实现泛型工具 Merge
将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。
type foo = {
name: string;
age: string;
};
type bar = {
age: number;
sex: string;
};
type Result = Merge<foo, bar>;
//type Result = {
// name: string;
// age: number;
// sex: string;
//}
type Merge<F, S> = {
// 遍历所有的 key,联合类型默认会去重
[P in keyof F | keyof S]: P extends keyof S // 如果P包含在keyof S中
? // 直接取后者的值的类型,保证后者类型覆盖前者
S[P]
: // 如果是前者的属性
P extends keyof F
? // 返回前者的类型
F[P]
: // 不会走到这一流程
never;
};
分布式条件特性
分布(分发)式条件特性
条件类型在结合联合类型+泛型使用时,会触发分布式条件特性
| 分布式条件类型 | 等价于 | | -------------------------- | -------------------------- | --------------------------- | --------------------------- | -------------------------- | ---------------------------- | | string extends T ? A : B
| string extends T ? A : B
| | (string | number) extends T ? A : B
| (string extends T ? A : B) | (number extends T ? A : B)
| | (string | number | boolean) extends T ? A : B
| (string extends T ? A : B) | (number extends T ? A : B) | (boolean extends T ? A : B)
|
上节课不是写过这样的类型工具吗
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'abc'>; // true
type D = IsString<123>; // false
如果我们传入的T
是一个联合类型,那么就会触发分布式特性
type IsString<T> = T extends string ? 1 : 2;
type E = IsString<'a' | true | 1>; // 1 | 2
我们可以写的再灵活一些。比如我们定义下面的类型:
type MyInclude<T, U> = T extends U ? T : never;
我们可以这样使用:
type A = 'a' | 'b' | 'c';
type B = 'a' | 'b';
type C = MyInclude<A, B>; // a | b
其实MyInclude
干了类似于下面的事情:
type C =
| MyInclude<'a', 'a' | 'b'>
| MyInclude<'b', 'a' | 'b'>
| MyInclude<'c', 'a' | 'b'>;
我们可以替换为具体的定义来理解一下:
type C =
| ('a' extends 'a' | 'b' ? 'a' : never)
| ('b' extends 'a' | 'b' ? 'b' : never)
| ('c' extends 'a' | 'b' ? 'c' : never);
这样其实得到结果:
type C = 'a' | 'b' | never;
最后根据 never 的特性,直接省略掉,得到最后的结果
type C = 'a' | 'b';
上面MyInclude
这个代码例子其实完全可以反过来,又形成另外一个类型:
type MyExclude<T, U> = T extends U ? never : T;
type A = 'a' | 'b' | 'c';
type B = 'a' | 'b';
type C = MyExclude<A, B>; // c
大家可以按照上面的步骤,自行分析一下
MyInclude
实际上是Extract<Type, Union>工具类型的实现
MyExclude
实际上是Exclude<UnionType, ExcludedMembers>工具类型的实现
根据Exclude<UnionType, ExcludedMembers>和Pick<Type, Keys>工具还能实现和Pick<Type, Keys>工具相反的效果
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Foo = {
name: string;
age: number;
};
type Bar = MyOmit<Foo, 'age'>; //{ name: string }
MyOmit 的实现,其实就是Omit<Type, Keys>工具类型的实现
这几个工具,我们可以做个案例来练习一下,比如有如下对象字面量类型:
type User = {
id: number;
name: string;
age: number;
tel: string;
address: string;
};
现在希望实现一个工具类型,将选择键名设置为可选,比如,如果设置 age,tel 和 address,那么经过工具类型转换之后,上面的类型别名就会变为:
type User = {
id: number;
name: string;
age?: number;
tel?: string;
address?: string;
};
// type RequiredPick = Omit<User, "age"|"tel"|"address">
// type PartialPick = Partial<Pick<User, "age" | "tel" | "address">>
// type OptionalPick = RequiredPick & PartialPick
// let user: OptionalPick = {
// id: 1,
// name: "John",
// }
type OptionalPick<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
let user: OptionalPick<User, 'address' | 'age' | 'tel'> = {
id: 1,
name: 'John',
};
最后,触发分布式条件类型需要注意两点:
1、类型参数需要通过泛型参数的方式传入,也就是下面这种直接写死的是不行的
// 始终都是"no"
type A = string | number | boolean extends string | number ? 'yes' : 'no';
2、类型参数需要是一个联合类型,并且条件中的泛型参数不能被包裹,比较下面两个结果的区别
type B<T> = T extends any ? T[] : never;
type C<T> = [T] extends any ? T[] : never;
type D = B<string | number>; // string[] | number[]
type E = C<string | number>; // (string | number)[]Ò
映射类型的属性过滤
映射类型的属性过滤
上面我们通过Pick
+ Exclude
实现了Omit
类型工具,那我们能不能完全自己实现,不借助已有的类型工具呢?也可以,不过我们需要掌握一个技巧:通过as + never
实现属性过滤的效果
type User = {
readonly id: number;
name: string;
tel: string;
address?: string;
};
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type A = MyOmit<User, 'tel' | 'address'>; // {readonly id: number; name: string}
在例子中,映射 K in keyof T
获取类型 T 的每一个属性以后,后面紧跟着as
其实是可以为键重新映射命名的
不过现在,它的键名重映射 as P extends K ? never : P
,使用了条件运算符,又会触发分布式处理
"id" ---> "tel" | "address" ? never : "id"
"name" ---> "tel" | "address" ? never : "name"
"tel" ---> "tel" | "address" ? never : "tel"
"address" ---> "tel" | "address" ? never : "address"
"id" | "name" | never | never ---> "id" | "name"
我们还能再升级一下,比如:只保留 User 值类型是 string 类型的,生成新的类型
type PickStringValueType<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type FilteredUser = PickStringValueType<User>; //{name:string, tel:string}
当然,你想反过来,去掉值类型是 string 类型的,将K
和never
换个位置就行了
其实上面做的更加普遍性一些,就完全可以写成一个类型工具:
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P];
};
type B = PickByType<User, number>; // { readonly id: number }
infer
infer
通过使用infer
关键字,还可以在条件类型中声明泛型类型。
// type Flatten<T> = T extends any[] ? T[number] : T;
type Flatten<T> = T extends (infer U)[] ? U : T;
type T1 = Flatten<number[]>; // number
type T2 = Flatten<string>; // string
const arr = [
{ id: 1, name: 'aaa' },
{ id: 2, name: 'bbb' },
{ id: 3, name: 'ccc' },
];
// 对象字面量类型 {id: number, name: string}
type T3 = Flatten<typeof arr>;
对比之前方括号运算符T[number]
其实使用infer
关键字之后,我们的类型代码更易读了。如果你不是对方括号运算符那么的熟悉,T[number]
的写法本身就很具有迷惑性。
infer
,意为推断,如 infer U
中 U
就表示 待推断的类型,你完全可以先把这里的infer U
看做any
,当执行时,typescript 推导出具体的类型,并将类型赋值给U
比如,我们希望获取数组第一个元素的类型:
type arr1 = ['a', 'b', 'c'];
type arr2 = [3, 2, 1];
type F1 = First<arr1>; // 'a'
type F2 = First<arr2>; // 3
我们可以通过 infer 进行推断,把第一个元素和其他元素分开,再连成一个数组就好。
type First<T extends any[]> = T extends [infer F, ...infer R] ? F : never;
当然,其实也可以用T[K]
,使用方括号运算符
type First<T extends any[]> = T extends [] ? never : T[0];
T[0]
其实就是获取第 0 个位置上元素的类型,这里判断T
和一个空元组的兼容性,也就是 T 不是一个空元组,那就得到第 0 个位置上元素的类型。
其实还能有下面的写法:
type First<T extends any[]> = T['length'] extends 0 ? never : T[0];
T['length']
可以获取 length 属性的类型,其实也就是数组长度,不是 0 的话,得到第 0 个位置上元素的类型
type ArrayLength<T extends any[]> = T['length'];
type L1 = ArrayLength<arr1>; // 3
继续,交换元组两个位置上的类型
type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;
type S1 = Swap<[1, 2]>; // 符合元组结构,首尾元素替换[2, 1]
type S2 = Swap<[1, 2, 3, 4]>; // 不符合元组结构,直接返回原数组[1,2,3,4]
当然,如果你希望无论如何数组的首位都进行交换,一样简单,加上**...
操作符**即可
type Swap<T extends any[]> = T extends [infer A, ...infer Rest, infer B]
? [B, ...Rest, A]
: T;
同样,函数也能进行推断
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// string
type A = GetReturnType<() => string>;
// void
type B = GetReturnType<(n: number) => void>;
// number
type C = GetReturnType<() => number>;
GetReturnType
实际上是ReturnType<Type>
的具体实现
模板字符串类型
模板字符串类型
TS 字符串模板类型的写法跟 JS 模板字符串非常类似
type World = 'world';
type Greeting = `hello ${World}`;
除了前面的 type
跟 JS
不一样之外,后面就是一模一样了,通过 ${}
包裹,里面可以直接传入类型变量,使用变量的模板字符串可以实现你意想不到的效果。
type Direction = 'left' | 'right' | 'top' | 'bottom';
type BoxName = 'padding' | 'margin' | 'border';
type BoxModel = `${BoxName}-${Direction}`;
使用模板字符串,联合类型会被挨个组合到模板中,最后轻松的生成一个包含各种组合的联合类型
使用对象也能处理一些更多的内容:
const person = {
firstName: 'John',
lastName: 'Doe',
age: 30,
};
type PersonKeys = keyof typeof person;
type EventPersonChange = `${PersonKeys}Changed`;
// 泛型处理
// keyof T 默认会认为对象的键有string|number|symbol
// keyof T & string 相当于 (string|number|symbol) & string ---> string
type EventObjectChange<T> = `${keyof T & string}Changed`;
type P = EventObjectChange<typeof person>;
加入映射类型:
type A = {
foo: number;
bar: number;
};
type B = {
[K in keyof A as `${K}ID`]: number;
};
// 等同于
// type B = {
// fooID: number;
// barID: number;
// }
但是如果想做的通用一点,也就是和泛型结合,会遇到问题:
// 结合泛型使用,由于keyof T得到的是一个联合类型,不能直接用于模板字符串拼接
// 需要使用 交叉类型 &,去掉其他类型,只保留字符串类型
type AddID<T> = {
[K in keyof T as `${K & string}ID`]: number;
};
type D = AddID<A>;
Typescript 官方也提供了很多内置的字符串工具Intrinsic String Manipulation Types,根据名字大概也能猜测出意思
type World = 'world';
type Greeting = `hello ${World}`;
type UpperCaseGreeting = Uppercase<Greeting>; // `HELLO ${Uppercase<World>}`;
// type Greeting = "HELLO WORLD"
type LowerCaseGreeting = Lowercase<Greeting>;
// type LowerCaseGreeting = "hello world"
type CapitalizeGreeting = Capitalize<LowerCaseGreeting>;
// type CapitalizeGreeting = "Hello world"
type UnUpperCaseGreeting = Uncapitalize<UpperCaseGreeting>;
// type CapitalizeGreeting = "hELLO WORLD"
这还仅仅是字符串模板的初级使用,结合这泛型编程,可以玩出很多花样
比如提供一个对象字面量类型,通过字符串模板直接得到 Getter 和 Setter 类型
type User = { name: string; age: number; address: string };
type AddGetter<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type AddSetter<T> = {
[K in keyof T as `set${Capitalize<K & string>}`]: (arg: T[K]) => void;
};
type UserGetter = AddGetter<User>;
type UserSetter = AddSetter<User>;
还可以处理的更通用一些:
type ObjectWithGetterSetter<T extends object> = T & AddGetter<T> & AddSetter<T>;
type UserWithGetterSetter = ObjectWithGetterSetter<User>;
let p: UserWithGetterSetter = {
name: 'jack',
age: 20,
address: '北京',
getName() {
return this.name;
},
getAge() {
return this.age;
},
getAddress() {
return this.address;
},
setName(name: string) {
this.name = name;
},
setAge(age: number) {
this.age = age;
},
setAddress(address: string) {
this.address = address;
},
};
递归复用
递归复用
现在有这么一个需求,需要将字符串字面量类型中的每个值类型取出,组成联合类型,类型于:
type A = '12345';
转变为;
type B = '1' | '2' | '3' | '4' | '5';
如果字符串字符串长度不变,那我们可以直接使用infer
进行类型推断
type A = '12345';
type StringToUnion<S extends string> =
S extends `${infer One}${infer Two}${infer Three}${infer Four}${infer Five}`
? One | Two | Three | Four | Five
: never;
type B = StringToUnion<A>;
但是这仅仅才 5 个字符串,如果字符串较多的话,不是要infer
推断一堆类型,比如来个九字真言,难道要infer9
次?
type A = '临兵斗者皆阵列前行';
这个时候我们就可以使用递归复用:当处理数量较多的类型的时候,可以只处理一个类型,然后递归的调用自身处理下一个类型,直到结束条件
type NineMantra = '临兵斗者皆阵列前行';
type StringToUnion<S extends string> = S extends `${infer One}${infer Rest}`
? One | StringToUnion<Rest>
: never;
type NineMantraUnion = StringToUnion<NineMantra>;
和字符串字面量类型很类似的,如果一个数组要做一些类似的类型处理,那一样可以递归,比如,我们要把数组中的元素类型倒序
type ReverseArr<T extends any[]> = T extends [
infer One,
infer Two,
infer Three,
infer Four,
infer Five
]
? [Five, Four, Three, Two, One]
: never;
type Reversed = ReverseArr<[1, 2, 3, 4, 5]>; // [5, 4, 3, 2, 1]
同样,我们使用递归复用:
type ReverseArr<T extends any[]> = T extends [infer One, ...infer Rest]
? [...ReverseArr<Rest>, One]
: T; // 注意结束之后返回的是数组
type Reversed = ReverseArr<[1, 2, 3, 4, 5]>; // [5, 4, 3, 2, 1]
再来一个,比如,我们现在通过编写一个类型工具,获取一个字符串字面量类型的长度
type S = LengthOfString<'12345'>; // 5
我们可以思考,之前我们讲过数组类型是不是可以获取长度,通过 T['length'],那我们能不能把字符串类型转成数组类型呢?完全可以,通过 infer 推断和递归复用:
type LengthOfString<
S extends string,
T extends string[] = []
> = S extends `${infer F}${infer R}`
? LengthOfString<R, [...T, F]>
: T['length'];
type S = LengthOfString<'12345'>;
通过递归复用,还能实现对索引映射类型的深递归,比如。我们希望将一个层级较深的对象类型全部属性转为readonly
只读
type User = {
id: number;
name: string;
address: {
province: string;
city: {
name: string;
street: string;
};
};
};
如果我们使用之前写的 MyReadonly 处理,仅仅只会把第一个层级的属性转变为readonly
type MyReadonly<T> = {
readonly [key in keyof T]: T[key];
};
type ReadonlyUser = MyReadonly<User>;
这里我们简单使用递归就能实现想要的效果
type DeepReadonly<T extends Record<string, any>> = {
readonly [K in keyof T]: T[K] extends Record<string, any>
? DeepReadonly<T[K]>
: T[K];
};
type ReadonlyUser = DeepReadonly<User>;
不过这样不好看到最后转换的效果,因为 TS 为了保证性能,并不会做深层的计算。
有一个比较实用的类型体操技能,就是在比较复杂的,特别是需要递归计算的类型体操计算外,包裹一层代码:
T extends any ?
具体类型体操代码
: never
这样我们就可以看到最后计算完成的效果,比如把上面的代码换成:
type DeepReadonly<T extends Record<string, any>> = T extends any
? {
readonly [K in keyof T]: T[K] extends Record<string, any>
? DeepReadonly<T[K]>
: T[K];
}
: never;
type ReadonlyUser = DeepReadonly<User>; //现在可以看到全部计算完成的类型效果
分发逆变推断
分发逆变推断
根据函数的型变,可以做出一些比较复杂的类型体操变化
实现高级工具类型函数:联合类型转为交叉类型
type I = UnionToIntersection<{ id: 1 } | { name: 'jack' } | { sex: '男' }>;
// { id: 1 } & { name: "jack" } & { sex: "男" }
在所有类型转换中,联合转交叉可以说是比较有难度的了
核心在于其他类型都有比较简单的遍历方法,比如元组的 T extends [infer F, ...infer R]
,对象的 [P in keyof T]: T[P]
,还有字符串的遍历套路,在这些类型中,转交叉其实非常简单。这里以元组为例:
type TupleToIntersection<T extends any[]> =
// 递归复用遍历
T extends [infer F, ...infer R]
? // 元素交叉即可
F & TupleToIntersection<R>
: unknown; // any & unknown = any 所以当 T 为空时,返回 unknown不影响结果
// MyType = {id: 1} & {name: 'jack'}
type MyType = TupleToIntersection<[{ id: 1 }, { name: 'jack' }]>;
但是对联合类型就麻烦了,因为我们无法把联合类型一个一个拉出来进行遍历,联合类型只有分布式(分发)特性。但是分发特性也是从一个联合类型返回一个新的联合类型,并不能转成交叉类型。
那么这个题,可以通过利用联合类型的分布式特性
+ 逆变特性
+ infer类型推断
实现这个效果
type UnionToIntersection<U> =
// 利用分发特性生成
// (arg: { id: 1 }) => any |
// (arg: { name: "jack" }) => any |
// (arg: { sex: "男" }) => any
(U extends any ? (arg: U) => any : never) extends (arg: infer P) => any
? P // 利用逆变特性,P = { id: 1 } & { name: "jack" } & { sex: "男" }
: never;
函数参数逆变的特性不知道大家有没有忘记:
type C = { id: 1; name: 'jack'; sex: '男' } extends { id: 1 } ? 1 : 2; // 1
type D = ((arg: { id: 1 }) => any) extends (arg: {
id: 1;
name: 'jack';
sex: '男';
}) => any
? 1
: 2; //1