前言
用TS好几年了,但一直没时间研究其内部实现。这篇文章用于探讨TS官网Utility Types
章节提供的高阶类型工具方法的内部实现。
操作符相关
在探讨具体内部实现时,我们首先需要关注几个操作符extends
| infer
| keyof
| in keyof
| typeof
| Indexed Access Types
的作用,具体可以查看官网类型操作章节。
extends
extends的使用场景有两种类型继承
| 条件类型
- 条件类型:
SomeType extends OtherType ? TrueType : FalseType
(SomeType类型可以分配给OtherType时为true否则为false) - 如果给定类型 SomeType
扩展了另一个给定类型
OtherType,则 ConditionalType 为 TrueType,否则为 FalseType。 - extends 意味着 SomeType 类型的任何值也是 OtherType子类型
// 1. 类型继承
interface A {
name:string;
};
interface B extends A {
age: number;
}
// 结果: B => {name:string; age: number;}
// 2. 条件类型
`SomeType extends OtherType ? TrueType : FalseType;`
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// 结果: type Example1: number。 因为DOg继承于Animal
type Example2 = RegExp extends Animal ? number : string;
// 结果: type Example2: string。
infer
最开始类型推断infer
表示在 extends
条件语句中待推断的类型变量
。 在之后的版本更新内置了infer相关的映射类型提取函数的返回值类型
| 提取构造函数中的参数类型
1. 在 `extends` 条件语句中`推断的类型变量`
type IParam<T> = T extends (A: infer R ) => void ? R : any;
type A = (info: number) => void
type B = IParam<A>;
// 结果: B: number
2. 提取函数的返回值类型
type IReturnType<T> = T extends (...args: any) => infer R ? R : any;
type A = (info: number) => void
type B = IReturnType<A>;
// 结果 B:void。 推断出函数返回值
keyof
keyof用于对象类型并且生成数字字面量
或者字符串
的联合类型 (直白就是获取到对象的key组成一个新的联合类型)
type Point = { x: number; y: number, 1:string };
type P = keyof Point;
// 结果: P => 'x' | 'y' | 1
in keyof
其通常用于将一个联合类型映射到对象属性中。
type Keys = 'name' | 'age'
type A = {
[key in keyof Keys]:number
}
// 结果: => A:{ name:number, age:number }
typeof
与JavaScript中的typeof类似,我们可以在类型上下文中引用变量或者属性的类型
let s = "hello";
let n: typeof s;
// 结果: let n:string
Indexed Access Types
我们可以使用索引访问类型
来查找另一种类型的特定属性
// 当我们想获取Person对象下某个属性的类型
type Person = { age: number; name: string; alive: boolean };
type Age = Person['age'] // 结果: type Age:number;
// 结合keyof获取该对象所有的属性下的类型集合
type I2 = Person[keyof Person]; // type I2 = string | number | boolean
// 当然你可能只需要age和name的类型集合
type I3 = Person["age" | "name"]; // type I3 = string | number
内置类型工具
这里只分析一下高频的类型定义,针对上述的每一种操作符都会涉及到。其他有兴趣的同学可自行查看官网.
Partial
Partial<T>将泛型T下的所有属性都变成可选。
keyof T
获取到的类型为 'name' | 'age',并且将'name' | 'age' 映射给泛型P- 通过
索引访问类型
获取到对应属性的类型,例如Info['name']的类型就为string
// 源码实现:
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 应用
interface Info {
name:string;
age:number;
}
Type Info1 = Partial<Info>
Info1 => {name?: string; age?: number;}
Required
Required<T>正好与Partial<T>相反,其将泛型T下的所有属性都变成必选。
- 大体他逻辑与上面一致。这里侧重讲一下
-?
的含义。-?
意味着所有属性必须存在
,又名删除可选性?
// 源码实现:
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 应用
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
const obj2: Required<Props> = { a: 5 }; `报错,缺少b属性`
Readonly
Readonly<T>其将泛型T下的所有属性都变成只读状态,即不能修改其值。
- 通过在属性前面添加
readonly
关键字,来声明该属性变为只读属性
// 源码实现:
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"; `!!!报错啦,title为只读属性`
Record
Record<Keys, Type>,联合类型keys作为对象的key,Type类型作为对象属性的类型。在日常中我们声明一个未知对象通过Record<string, unknown>
- 泛型
K
类型继承keyof any
. 那么keyof any
是什么呢? 其值为string | number | symbol
。
// 源码实现:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// 应用
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
};
Pick
Pick<T, K> 从T中选择一组属性,其键位于并集 K 中。(Pick的含义就是提取的意思)
- 泛型
K
的值限定keyof T
,也就是说K
是keyof T的子集
,例如keyof T为'name' | 'age'那么K
不可能超出这两个元素。
// 源码实现:
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">; // description属性被排除了
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
Exclude
Exclude<T, U>从 T 中排除那些可分配给 U 的类型
T extends U
还记得前面提到条件语句
吗,遍历T中的所有子类型
,如果该子类型约束于U(存在于U、兼容于U)。存在则返回never类型,不存在则返回子类型
tips: Extract<T, U>与Exclude<T, U>正好相反,T extends U ? T : never
满足条件返回子类型
// 源码实现:
type Exclude<T, U> = T extends U ? never : T;
// 应用
type T0 = Exclude<"a" | "b" | "c", "a">;
// T0 => "b" | "c"
Omit
Omit<T, K> 构造一个具有T属性(类型 K 中的属性除外
)的类型。结合Pick和Exclude实现,拆成如下两步
- 首先Exclude<keyof T, K>排除掉K中属性,得到P
- Pick<T, P>提取T符合P中的属性,构建成一个新的类型
// 源码实现:
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" | "completed">;
const todo: TodoPreview // TODO 为 {title: string}
Parameters
Parameters<T> 获取到函数中的参数类型
- T extends (...args: any) => any表明泛型T必须是一个函数
- 如果T满足(...args: any) => any条件则返回P类型,否则返回never。这个P是通过
infer推断出来的类型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
其他
使用?.
可选操作符
const a = { }
如果直接获取a.b.d程序会报错,但是使用a?.d?.c程序会返回undefined
使用!.
排除掉undefined|null标识这个值一定存在,有点断言as的味道
type IA = string
type IB = {
name: string | undefined
};
const name1: IB = {
name: '1'
};
const name2: IA = name1.name;
直接如上写会类型报错: Type string | undefined is not assignable string .....
const name2: IA = name1.name!;
在name后追加!标识这个name不可能是undefined或者null
typescript中-?
的意味着所有的属性都必须存在,又名删除可选属性
type T = {
a: string
b?: string
}
const sameAsT: { [K in keyof T]: string } = {
a: 'asdf', // a is required
}
// Note a became optional
const canBeNotPresent: { [K in keyof T]?: string } = {
}
// Note b became required
const mustBePreset: { [K in keyof T]-?: string } = {
a: 'asdf',
b: 'asdf' // b became required
}
const 与 Readonly的区别
它们实际上都做同样的事情,但一个用于变量,另一个用于属性
。
变量`const`不能被重新赋值,就像`readonly`属性一样。
本质上,当您定义属性时,您可以使用它`readonly`来防止重新分配。这实际上只是一个编译时检查。
当您定义`const`变量(并以更新版本的 JavaScript 为目标以保留`const`在输出中)时,也会在运行时进行检查。
interface 与 Type的区别
如下是官网提供的差异。几乎所有的interface功能都可以使用type来实现。最主要的区别是Type类型无法重新打开去添加新的属性,而接口类型总是扩展的
CONST断言
TypeScript 3.4 引入了一个名为 const 断言的字面值的新构造。
let name = 'AKclown' as const; // name的类型是'AKclown'
// 没有const断言
let name = 'x'; // name的类型是string
元组
规定数组对应元素的数据类型
// 两个元素,第一个元素为string类型、第二个元素为number
type info = [string, number];
// 第一个元素为string,其余元素为number类型
type info = [string, ...number[]];
tsc编译与babel的差异
图片引用 神说要有光
的文章编译 ts 代码用 tsc 还是 babel?