1. 泛型中的 K/T/V
视频地址:https://www.bilibili.com/video/BV1sY4y1H7vk
2. TS 中的联合类型
视频地址:https://www.bilibili.com/video/BV1jY4y1i7Lh
经常需要做类型缩窄,保证安全访问其中的一些对象或方法:
3. TS 中的类型是什么
视频地址:https://www.bilibili.com/video/BV1Zq4y1a79K
类型:指一系列值的集合,比如 number 类型是所有数字的集合。
单元类型:单个值的类型,比如 type A = 'A'
中的类型 A。
多个值的类型:如 boolean
,值为 true
和 false
。
无限集合:如 string 或 number。
结构化类型:
4. TS 中的函数重载
视频地址:https://www.bilibili.com/video/BV1JS4y1Y7Jy
函数重载分为:函数签名和实现签名。
重载签名:定义函数中每个参数的类型和函数的返回值类型,但不包含函数体,一个函数可以有多个重载签名,实际使用时只有重载签名能调用。
实现签名:通常会是更具体通用的类型,一个函数只能有一个实现签名。
重载匹配过程:
还可以重载类方法:
5. TS 中的索引类型
视频地址:https://www.bilibili.com/video/BV1RY411A7YS
当需要使用已知的 key 和 value 定义对象类型时,可以使用:
- 索引类型;
Record 工具类型。
let user = {}
user.id = '123' // Error: Property 'id' does not exist on type '{}'
user.name = '321' // Error: Property 'name' does not exist on type '{}'
1. 索引类型
索引类型语法:
{ [ key: KeyType ] : ValueType }
注意点:索引签名参数类型(
KeyType
)必须是string
/number
/symbol
或模版字面量类型(TS4.1引入)。
- TS 会对索引签名类型进行隐式转换:
2. Record 工具类型
文档地址:https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type
通过已知key和value创建类型:
type A = Record<string, number>
let a: A = {'aa': 123}
3. 索引类型 VS Record 工具类型
索引签名参数类型(KeyType
)只能是 string
/ number
/symbol
或模版字面量类型。
Record 的参数类型还可以是字面量类型和字面量类型组成的联合类型。
6. TS 中的 keyof 操作符
视频地址:https://www.bilibili.com/video/BV1nT4y1a7AR
文档地址:https://www.typescriptlang.org/docs/handbook/2/keyof-types.html
TS2.1 新增,用来获取某种类型上的所有键,并返回联合类型。
获取以后可以通过索引进行访问:
举例:
7. 映射类型
视频地址:https://www.bilibili.com/video/BV1Wr4y1J7x3
文档地址:https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
是一种泛型类型,用来将一种类型映射成另一种类型。
语法如下:
{ [ P in K ] : T }
// in -> 类型 for...in 语句
// T-> 任意类型
还可以使用 readonly
和 ?
修饰符:
{ readonly [ P in K ] ? : T }
// readonly -> 表示添加只读修饰符
// ? -> 表示移除可选修饰符
TS4.1 增加 as 子句,对映射类型的 key 进行重映射:
其中,NewKeyType
类型必须是:string
、number
、symbol
联合类型的子类型。
通过返回 never
类型来过滤一些类型:
8. any 和 unknown 区别
视频地址:https://www.bilibili.com/video/BV1WR4y1P7dw
any 和 unknown 都是顶级类型,但是 unknown 更加严格,不像 any 那样不做类型检查,反而 unknown 因为未知性质,不允许访问属性,不允许赋值给其他有明确类型的变量。
any 类型可以理解成我不在乎它的类型,unknown 类型可以理解成我不知道它的类型。
let foo: any = 123;
console.log(foo.msg); // 符合TS的语法
let a_value1: unknown = foo; // OK
let a_value2: any = foo; // OK
let a_value3: string = foo; // OK
let bar: unknown = 222; // OK
console.log(bar.msg); // Error
let k_value1: unknown = bar; // OK
let K_value2: any = bar; // OK
let K_value3: string = bar; // Error
bar 是个未知类型,任何类型都可以赋给 unknown 类型,所以无法确定是否有 msg 属性,因此 TS 提示错误。
unknown 类型的值只能赋值给 any 和 unknown 类型的变量。
1. 联合类型中的 unknown
包含 unknown 类型的联合类型,相当于 unknown 类型。
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
但如果包含 any,则会相当于 any 类型。
type UnionType5 = unknown | any; // any
2. 交叉类型中的 unknown
每种类型都可以赋值给 unknown 类型,所以在交叉类型中包含 unknown 不会改变结果。
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
3. keyof 操作符和映射类型
type T40 = keyof any; // string | number | symbol
type T41 = keyof unknown; // never
type T50<T> = { [P in keyof T]: number } // { [P in keyof T]: number; }
type T51 = T50<any> // { [x: string]: number; }
4. unknown 类型缩窄
// 1. typeof
const f = (cb: unknown) => {
if(typeof cb === 'function'){
cb();
}
}
declare function isFunction (x: unknown) : x is Function;
// 2. instanceof
// 3. 自定义类型守卫
const f = (x: unknown) => {
if(x instanceof Error){
x;// Error
}
if(isFunction(x)){
x;// Function
}
}
总结
9. TS 中交叉类型
视频地址:https://www.bilibili.com/video/BV1RF411T7zu
交叉类型满足以下特征:
- 唯一性:
A & A
等价于A
; - 满⾜交换律:
A & B
等价于B & A
; - 满⾜结合律:
(A & B) & C
等价于A & (B & C)
; - ⽗类型收敛:如果 B 是 A 的⽗类型,则
A & B
将被收敛成 A 类型: ```typescript type A0 = number & 1; // 1 其中 number 是 1 的父类型 type A1 = string & “1”; // “1” type A2 = boolean & true; // true
type A3 = any & 1 // any type A4 = any & boolean // any type A5 = any & never // never
另外,除了 `never`类型交叉运算结果为 `never`类型外,任何类型和 `any` 类型交叉运算,结果都是 `any` 类型。
在多个对象类型做交叉运算的时候,如果存在相同属性,且属性类型为**非基本数据类型**,则**可以进行合并**。
```typescript
type A1 = {
name: string;
age: number;
}
type A2 = {
location: string;
}
type A3 = A1 & A2;
/*
type A3 = {
name: string;
age: number;
location: string;
}
*/
若相同属性是可辨识的属性(即类型是字面量类型或者字面量类型组成的联合类型),则最后结果为 never
类型。
type A = { kind: 'a', foo: string };
type B = { kind: 'b', foo: number };
type C = { kind: 'c', foo: number };
type AB = A & B; // never
type BC = B & C; // never
函数类型也支持交叉运算:
type F1 = (a: string, b: string) => void;
type F2 = (a: number, b: number) => void;
var f: F1 & F2 = (a: string | number, b: string | number) => { };
f("hello", "world"); // Ok
f(1, 2); // Ok
f(1, "test"); // Error
第三个报错是因为 TypeScript 编译器利用函数重载特性来实现不同函数类型的交叉运算。
可以再实现一个新的函数类型来解决该问题:
type F1 = (a: string, b: string) => void;
type F2 = (a: number, b: number) => void;
type F3 = (a: number, b: string) => void;
var f: F1 & F2 & F3 = (a: string | number, b: string | number) => { };
f("hello", "world"); // Ok
f(1, 2); // Ok
f(1, "test"); // Ok
小结一下,结合映射类型可以实现 PartialByKeys 工具类型,将指定 keys 变成可选:
type User = {
id: number;
name: string;
age: number;
}
type PartialByKeys<T, K extends keyof T> = Simplify<{
[P in K]?: T[P]
} & Pick<T, Exclude<keyof T,K>>>
type U1 = PartialByKeys<User, "id">
type U2 = PartialByKeys<User, "id" | "name">
类似的可以这么实现 RequiredByKeys
工具类型:
type Simplify<T> = { [K in keyof T]: T[K]}
type User = {
id?: number;
name?: string;
age?: number;
}
type RequiredByKeys<T, K extends keyof T> = Simplify<{
[P in K]-?: T[P]
} & Pick<T, Exclude<keyof T,K>>>
type R1 = RequiredByKeys<User, "id">
type R2 = RequiredByKeys<User, "id" | "name">
10. TS 中条件类型
视频地址:https://www.bilibili.com/video/BV1HR4y1N7ea
TS 条件类型语法如下:
T extends U ? X : Y
即当被检查类型(类型 T
)可以赋值给类型 U
时,返回类型 X
,否则返回类型Y
。看个简单示例:
type IsString<T> = T extends string ? true : false;
type I0 = IsString<number>; // false
type I1 = IsString<"abc">; // true
type I2 = IsString<any>; // boolean
type I3 = IsString<never>; // never
并且在 TS 内置工具类型中:Exclude
/Extract
/NonNullable
/Parameters
/ReturnType
都使用到了条件类型。
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T extends null | undefined ? never : T;
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
当被检查类型(类型 T
)是联合类型的时候,就是分布式条件类型,即被检查的类型是个“裸”类型(没有被数组、元组或 Promise 包装过)时,该条件类型成为分布式条件类型,运算过程会分解成多个分支。
T extends U ? X : Y
// T => A | B | C
A | B | C extends U ? X : Y =>
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
举例如下:
type Naked<T> = T extends boolean ? "Y" : "N";
type T0 = Naked<number | boolean>; // "N" | "Y"
/*
执行流程
(number extends boolean ? "Y" : "N") // => "N"
| (boolean extends boolean ? "Y" : "N")// => "Y"
=> "N" | "Y"
*/
接下来就可以手写前面几个 TS 内置工具类型:
type MyExclude<T, U> = T extends U ? never : T;
type M1 = MyExclude<'a' | 'b', 'a'> // 'b'
type MyExtract<T, U> = T extends U ? T : never;
type M2 = MyExtract<'a' | 'b', 'a'> // 'a'
type MyNonNullable<T> = T extends null | undefined ? never : T;
type M3 = MyNonNullable<null> // never
type M4 = MyNonNullable<null | 'a' | undefined> // 'a'
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer R) => any ? R : never;
type M5 = MyParameters<(name: string, age: number) => any> // [name: string, age: number]
type MyReturnTypes<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
type M6 = MyReturnTypes<(name: string, age: number) => number> // number
结合映射类型,实现 FunctionProperties
和 NonFunctionProperties
工具类型:
// 提取对象中的函数名称
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface User {
id: number;
name: string;
age: number;
updateName(newName: string): void; }
type T5 = FunctionPropertyNames<User>; // "updateName"
type T6 = NonFunctionPropertyNames<User>; // "id" | "name" | "age"
type T7 = FunctionProperties<User>; // { updateName: (newName: string) => void; }
type T8 = NonFunctionProperties<User>; // { id: number; name: string; age: number; }
11. infer
视频地址:https://www.bilibili.com/video/BV1qv4y1P7D2
条件类型:检测两种类型之间的关系,通过条件类型判断两种类型是否兼容。
infer :用来声明类型变量,以存储在模式匹配过程中所捕获的类型。
1. 使用注意
infer 使用注意:
只能用在条件类型的 extends 子句中,并且 infer 声明的类型变量只在条件分支的 true 分支中使用。
type Wrong1<T> = (infer U)[] extends T ? U : T // Error
type Wrong2<T extends (infer U)[]> = T[0] // Error
type Wrong3<T> = T extends (infer U)[] ? T : U // Error
2. 函数重载的场景
在函数重载中,TypeScript 会使用最后一个调用前面进行类型推断:
3. 结合条件链
可以结合条件链实现更强大的功能:
4. 协变和逆变的场景
当使用 infer 声明多个类型变量时,若类型匹配,则会按顺序返回:
type User = {
id: number;
name: string;
}
type PropertyType<T> = T extends { id: infer U, name: infer R } ? [U, R] : T
type U2 = PropertyType<User> // [number, string]
而当只声明一个类型变量,却用在多个地方,则结果会不一样:
// 协变位置
type PropertyType2<T> = T extends {
id: infer U,
name: infer U
} ? U : T;
type U3 = PropertyType2<User> // string | number
这边返回string | number
。
需要注意的是,当使用同一个类型变量存储多个候选者,则:
- 当类型变量在协变位置,类型推断为联合类型;
- 当类型变量在逆变位置,类型推断为交叉类型。
这两个规则是 TS 规定的,所以不用太在意为何这样。
5. 练习题
联合类型转交叉类型:
type UnionToIntersection<U> = (
U extends any ? (arg: U) => void : never
) extends (arg: infer R) => void
? R
: never
type T1 = {a:'a'} | {b: 'b'}
type T2 = UnionToIntersection<T1> // { a: 'a'; } & { b: 'b'; }
type T3 = UnionToIntersection<number | string> // never
// U extends any ? (arg: U) => void : never 结果为
type U2 = ((arg: {a: 'a'}) => void) | ((arg: {b: 'b'}) => void)
12. type 和 interface 区别
视频地址:https://www.bilibili.com/video/BV1HB4y1y7KG
1. 相同点
- 都可以用来描述对象或函数 ```typescript // 类型别名 type type Point = { x: number, y: number }; type SetPoint = ( x: number, y: number ) => void;
// 接口 interface interface Point { x: number, y: number }; interface SetPoint { ( x: number, y: number ): void; }
2. 都支持拓展
```typescript
// 类型别名 type -> 通过交叉运算符 & 拓展
type T1 = { name: string };
type T2 = T1 & { honey: boolean };
const t: T2 = getBear();
t.name;
t.honey;
// 接口 interface -> 通过 extends 拓展
interface I1 { name: string };
interface I2 extends I1 {
honey: boolean;
}
// 接口 interface 通过 extends 拓展类型别名 type
interface I2 extends T1 {
honey: boolean;
}
// 类型别名通过交叉运算符 & 拓展接口 interface
type T2 = I1 & { honey: boolean };
2. 不同点
类型别名 type 用于基本类型、联合类型或元组类型定义,而接口 interface 不行。
type T1 = number;
type T2 = string | number;
type T3 = [number, number];
同名接口 interface 会自动合并,而类型别名 type 不会。 ```typescript // 同名接口合并 interface User { name: string }; interface User { age: number };
let user: User = { name: ‘leo’, age: 18 }; user.name; // ‘leo’ user.age; // 18
// 同名类型别名会冲突 type User = { name: string }; type User = { age : number }; // Error
利用同名接口合并的特点,可以在开发第三方库的时候,为开发者提供更好的安全保障。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/186051/1652658969108-15d29467-f700-4c01-9d32-c700dabef8db.png#clientId=ud095111f-5a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=316&id=u3f592b9c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=582&originWidth=1012&originalType=binary&ratio=1&rotation=0&showTitle=false&size=273507&status=done&style=none&taskId=uab7fc0e3-4ee6-4b21-a23f-471871b20bf&title=&width=549)
```typescript
interface ProtocolMap {
foo: { title: string }
bar: { name: string, age: number }
}
declare function onMessage <T extends keyof ProtocolMap> (type: T, handle: (arg: ProtocolMap[T]) => void): void;
onMessage('foo', (data) => { // data: { title: string; }
console.log(data.title)
})
onMessage('bar', (data) => { // data: { name: string; age: number; }
console.log(data.age)
console.log(data.title) // Property 'title' does not exist on type '{ name: string; age: number; }'
})
// 接口合并
interface ProtocolMap {
sendFile: { fileName: string, fileParams: { key: string, value: any} }
}
onMessage('sendFile', (data) => {
console.log(data.fileName, data.fileName)
})
13. 类型体操之 Pick
视频地址:https://www.bilibili.com/video/BV1Da411J7jj
实现一个 Pick 函数,用来挑选属性包含每个 keys 的值,返回新对象:
function Pick(obj, keys) {
const ret = {};
for (const key of keys) {
ret[key] = obj[key];
}
return ret;
}
const user = {
id: 666,
name: "阿宝哥",
address: "厦⻔",
};
const PickedUser = MyPick(user, ["id", "name"]);
重要:在 JS 中操作的是对象,在 TS 中操作的是类型。
类似的,定义一个 MyPick 函数,可以使用映射类型,它是一种泛型类型,用于把原有对象类型映射成新的对象类型,语法如下:
{ [ P in K ] : T }
语法意思不细讲,可以看前面。因此 MyPick 可以写作:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
14. 类型体操之 Omit
视频地址:https://www.bilibili.com/video/BV1254y1Z7BL
1. 应用场景
假设用户信息 User 对象接口如下:
其中的 id
、createdAt
和 updateAt
字段是由服务端自动添加上去,而前端注册用户只需要填 name
和password
字段,如何高效实现注册用户的 RegisterUser 类型?看看下图:
就可以使用 Omit
将对象类型中不需要的字段排除掉。
还可以用来通过利用接口继承方式实现覆盖已有对象类型中已知属性的类型:
这里将原来 createdAt
和 updateAt
类型改为字符串类型。
2. 如何实现
内部实现:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
3. 小结
Omit 是 Pick 的反向,和 Pick 一样都是为了解决代码复用问题。
15. TS 模版字面量类型(Template Literal Types)
视频地址:https://www.bilibili.com/video/BV1B34y1E7tm
文档地址:https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
1. 遇到问题
定义以下两种类型时,存在许多重复问题:
type CssPadding =
| 'padding-left' | 'padding-right'
| 'padding-top' | 'padding-bottom';
type CssMargin =
| 'margin-left' | 'margin-right'
| 'margin-top' | 'margin-bottom';
可以使用 TS4.1 引入的新的模版字面量类型:
type Direction = 'left' | 'right' | 'top' | 'bottom';
type CssPadding = `padding-${Direction}`;
type CssMargin = `margin-${Direction}`;
模板字面量类型以字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。语法类似 JS 模版字符串,使用时会替换模版中的变量 ${T}
,返回一个新的字符串字面量。
其中 T 类型可以为 string
/number
/boolean
或 bigint
类型。
2. 作用总结
- 将非字符串基本数据类型的字面量转换为对应的字符串字面量类型;
``typescript type EventName<T extends string> =
${T}Changed; type Concat<S1 extends string, S2 extends string> =
${S1}-${S2}; type ToString<T extends string | number | boolean | bigint> =
${T}`;
type T0 = EventName<”foo”>; // ‘fooChanged’ type T1 = Concat<”Hello”, “World”>; // ‘Hello-World’ type T2 = ToString<”阿宝哥” | 666 | true | -1234n>; // “阿宝哥” | “true” | “666” | “-1234”
如果 `EventName`/`Concat`工具类型的实际类型是联合类型,则:
```typescript
type T3 = EventName<"foo" | "bar" | "baz">;
// "fooChanged" | "barChanged" | "bazChanged"
type T4 = Concat<"top" | "bottom", "left" | "right">;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
因为当类型占位符的实际类型是联合类型,就会自动展开:
// 如 EventName
`[${A | B | C}]` => `[${A}]` | `[${B}]` | `[${C}]`;
如果是多个类型占位符,则多个类型占位符的联合类型解析为叉积:
// 如 Concat
`[${A|B},${C|D}]` =>
| `[${A},${C}]` | `[${A},${D}]`
| `[${B},${C}]` | `[${B},${D}]`;
- 处理字符串类型的内置工具类型;
如 Uppercase(所有字母大写)、Lowercase(所有字母小写)、Capitalize(首字母大写) 和 Uncapitalize(首字母小写)。
type GetterNmae<T extends string> = `get${Capitalize<T>}`;
type Cases<T extends string> = `${Uppercase<T>} ${Lowercase<T>} ${Capitalize<T>} ${Uncapitalize<T>}`;
type T5 = GetterNmae<'Foo'>; // "getFoo"
type T6 = Cases<'bar'>; // "BAR bar Bar bar"
结合 infer 关键字实现类型推断;
type Direction = "left" | "right" | "top" | "bottom";
type InferRoot<T> = T extends `${infer R}${Capitalize<Direction>}` ? R : T;
type T7 = InferRoot<"marginRight">; // "margin"
type T8 = InferRoot<"paddingLeft">; // "padding"
结合 as 子句对映射类型中的键进行重新映射;
文档地址:https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as
重映射的语法如下:
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// NewKeyType 的类型必须是 string | number | symbol 联合类型的子类型
}
可以实现一个 Getters 工具类型,用来为对象类型生成对应的 Getter 类型:
type User = { id: number, name: string };
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & T>}`]: () => T[K];
}
type UserGetter = Getters<User>;
// type UserGetter = {
// getId: () => number;
// getName: () => string;
// }
由于 keyof T
返回的类型可能包含 symbol
类型,而 Capitalize
工具类型要求处理的类型必须是 string
类型的子类型,所以这边需要通过交叉运算符进行类型过滤。
- 获取任意层级的属性的类型;
type PropType<T, Path extends string> = string extends Path
? unknown
: Path extends keyof T ? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T ? PropType<T[K], R>
: unknown
: unknown;
declare function getPropValue<T, P extends string>(
obj: T,
path: P
): PropType<T, P>;
const obj = { a: { b: { c: 666, d: "阿宝哥" } } };
let a = getPropValue(obj, "a"); // { b: {c: number, d: string } }
let ab = getPropValue(obj, "a.b"); // {c: number, d: string }
let abd = getPropValue(obj, "a.b.d"); // string
—- never 类型的妙用
先介绍 TS 中的 bottom type,它代表没有值的类型,也称为零类型或空类型,是所有类型的子类型,因此它可以赋值给其他类型,但任何类型都不能赋值给 never 类型(即没有任何类型是 never 类型的子类型),包括 any 类型。
看看下面代码:
由于 never 类型表示空类型,所以该类型不会包含任何值,因此:let n0: never = 1; // Type 'number' is not assignable to type 'never'.
let s0: never = 'leo'; // Type 'string' is not assignable to type 'never'.
let id: any = 'leo';
let s1: never = id; // Type 'any' is not assignable to type 'never'.
- 在联合类型中,never 类型会被剔除;
- 在交叉类型中,never 类型将覆盖其他类型,始终返回 never 类型。
type T0 = 1 | 2 | never // 1 | 2
type T1 = any | never // any
type T2 = number & never // never
type T3 = any & never // never
1. 利⽤ never 类型实现属性互斥
当需要实现函数参数互斥的情况,如下面示例,参数不能同时包含 id 和 name: ```typescript type User = { id?: never; // 设置为 never 类型从而过滤 name: string; }; type Id = { id: number; name?: never; // 设置为 never 类型从而过滤 };
declare function findUser(arg: Id | User): boolean;
const user = { name: “阿宝哥”, id: 666 }; const user1 = { name: “阿宝哥” }; const user2 = { id: 666 }; findUser(user); // Error findUser(user1); // Ok findUser(user1); // Ok
<a name="d95kz"></a>
### 2. 利⽤ never 类型实现属性关联
<a name="qxyRs"></a>
### 3. 利用 never 类型实现类型过滤
很多 TS 内置工具类型都使用到 never,如 Exclude、Extract 等等:
```typescript
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
// Exclude -> 利用 never 实现从 T 类型中排除 U 类型
type T4 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T5 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T6 = Exclude<string | number | (() => void), Function>; // string | number
我们也可以实现 NoNumbers
工具类型来排除联合类型中的数字类型:
type NoNumbers<T> = T extends number ? never : T;
type MyType = 1 | 2 | 3 | "a" | "b" | "c";
type MyTypeWithoutNumbers = NoNumbers<MyType>; // "a" | "b" | "c"
4. 利用 never 类型实现对象属性过滤
可以使用映射类型中 as 子句对键进行重映射,通过返回 never 类型对键进行过滤:
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
也可以实现 GetRequired 工具类型:
type GetRequired<T> = {
[K in keyof T as Omit<T, K> extends T ? never : K]: T[K]
}
type G0 = GetRequired<{ foo: number, bar?: string }>
type G1 = GetRequired<{ foo: undefined, bar?: undefined }>
5. 利用 never 类型限制 API 的使用
比如下面例子:
type Read = {}
type Write = {}
declare const toWrite: Write
declare class MyCache<T, R> {
put(val: T): boolean;
get(): R;
}
// 正常使用
const cache = new MyCache<Write, Read>()
cache.put(toWrite) // Ok
// 限制使用
declare class ReadOnlyCache<R> extends MyCache<never, R> {}
const readonlyCache = new ReadOnlyCache<Read>()
readonlyCache.put(data) // Error
通过把 put ⽅法参数的类型设置为 never 类型实现。
补充1. TS 中协变和逆变
主要是发生在子类型(subtype)和父类型(supertype)在安全类型转换后的兼容性。
在 TS 中,由于是采用结构化类型,只要类型的结构相同,便可以认为是同一类型。因此只要父子类型能够正常赋值即表示父子关系,表现形式有:
// extends
A extends B => A 是 B 的子类型
// 子类型可以赋值给父类型
B = A => A 是 B 的子类型
如果A,B代表两个类型;f()表示类型转换;A -> B表示A是B的子类。
- 当f()是协变时:若 A -> B,则f(A) -> f(B);
- 当f()是逆变时:若 A -> B,则f(B) -> f(A);
- 当f()是双变时:若 A -> B,则以上均成立;
- 当f()是不变时:若 A -> B,则以上均不成立,没有兼容关系;
以下面示例做介绍:
class Animal {
move(){
console.log("animal is moving");
}
}
class Cat extends Animal {
purr() {
console.log("cat is purring");
}
}
class WhiteCat extends Cat {
showoffColor() {
console.log("see my hair color");
}
}
其父子关系为:WhiteCat -> Cat -> Animal,根据父类兼容子类的原则可知:
let animal: Animal;
let cat: Cat;
let whiteCat: WhiteCat;
animal = cat;
animal = whiteCat;
cat = whiteCat;
3.1 问题
如果类型为 (param: Cat) => Cat ,那么它的类型兼容是什么?可以将该问题拆分成部分参数兼容性和返回值兼容性:
- (param: Cat) => void的兼容类型是什么?
- () => Cat的兼容类型是什么?
3.2 参数兼容性
let A1: (param: Cat) => void;
let A2: (param: WhiteCat) => void = (params: WhiteCat) => {
params.move();
params.purr();
params.showoffColor();
};
let A3: (param: Animal) => void = (params: Animal) => {
params.move();
};
A1 = A2; // 报错,A2 不能赋值给 A1,因为 A1 缺少 A2 必选属性 showoffColor
A1 = A3; // 成功
分析:
- A2 不能赋值给 A1,因为 A1 缺少 A2 必选属性
showoffColor
; - A3 能够赋值给 A1,因为 A1 包含了 A3 所有必须的属性,即 A3 的
move
属性。
A1 = A3
转换成 (param: Animal) => void -> (param: Cat) => void
,函数参数类型的父子类型发生反转,称为逆变。
3.3 返回值兼容性
let B1: () => Cat;
let B2 = () => new WhiteCat();
let B3 = () => new Animal();
B1 = B2; // 成功
const _b2 = B1();
_b2.move();
_b2.purr();
B1 = B3; // 报错,B3 不能赋值给 B1,因为 B3 缺少 B1 必选属性 purr
const _b3 = B1();
_b3.move();
_b3.purr();
分析:
- B2 能够赋值给 B1,因为 B1 包含了 B2 所有必须的属性,即
move
和purr
; - B3 不能赋值给 B1,因为 B3 缺少 B1 必选属性
purr
。
B1 = B2
转换为 () => WhiteCat -> () => Cat
,函数返回值类型的父子类型没有反转,称为协变。
3.3 现实中的函数参数类型
在 TS 中,参数类型既是协变又是逆变,称为双变,这并不安全,可以通过开启strictFunctionTypes
保证参数类型是逆变。
补充2. TS 中变量赋值检查
如果变量 B 要赋值给变量 A,则需要检查 A 类型中的属性是否在类型 B 中都有对应属性。
let a: { name: string } = { name: "jack" };
let b: { name: string; age: number; gender: string } = {
name: "jack",
age: 19,
gender: "man",
};
a = b; // ok b类型有a
b = a; // error 类型a 缺少类型b 中的以下属性: age, gender
需要特别注意的时候,调用函数时传入参数,如果直接将对象作为参数传入,会进行严格的类型定义:
function fun(user: { name: string }) {}
// 直接将对象作为参数传入
fun({name: "jack", age: 19, gender: "man"}); // error
// 先用变量赋值,再传入,就可以不经过额外属性检查
let myUser = {name: "jack", age: 19, gender: "man"};
fun(myUser); // ok
其将 myUser
这个变量赋值给 user
,根据上面的变量赋值类型兼容性,可以赋值成功,这样就可以绕开额外类型检查。
补充3. TS 中函数参数比较
函数参数的个数比较如下:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // a行 -> OK
x = y; // b行 -> Error `y`有个必需的第二个参数,但是`x`并没有,所以不允许赋值。
在 a 行中能正常赋值,因为函数 x 的每个参数都能在函数 y 中找到对应位置的参数,而 JS 额外参数可以忽略不传,因此可以正常赋值。
而 b 行报错,是因为函数 y 有第二个必填参数,但函数 x 上没有,因此无法赋值。
变量和函数类型的赋值,只会修改本身,而不会修改原有的类型。
举个实际例子:
let x = (a: number):void => {
console.log(a)
};
let y = (b: number, s: string):void => {
console.log(b+s)
};
y = x // ok
y(1, 'a') // 这时候调用,因为函数y已经被赋值为 (a: number) => void; 但是其类型还是(b: number, s: string) => void,所以还是要传两个参数,但是调用是只会用到第一个参数,这时候程序可以正常执行,打印结果是1
最后正常执行 y(1, 'a')
,因为函数 y 已经被赋值为 (a: number) => void;
但是其类型还是(b: number, s: string) => void
,所以还是要传两个参数,但是调用是只会用到第一个参数,这时候程序可以正常执行,打印结果是1