这里是TypeScript 全面进阶指南小册学习笔记。
Day 1 Starter
VS Code 好用的配置和插件
TypeScript Importer 收集项目内的类型定义,可以敲:,自动进行类型补全;同时还可以自动导入对应的类型文件。
Move TS 在对文件目录进行修改时,可以通过编辑文件路径,直接修改目录结构,同时将修改后的目录结构,同步到其他引入的文件中。
Vs Code 本身对ts语言的支持,在设置页搜索typescript字段,Function Like Return Types、 Parameter Names、Parameter Types、Variable Types这几个可以在调用函数时,可以显示函数定义相关的提示,比如函数返回值类型、函数入参名称、类型,虽然在页面上会多出信息,感觉在有些开发场景下还是很有用的。
ErrorLens 可以将VS Code 底部问题栏的错误下直接显示到代码文件中的对应位置
PlayGround
Playground 官方提供的ts开发环境,比较方便,可以直接写ts代码,并且查看编译后的js代码。 同时还支持ts版本等相关的配置,可视化进行tsconfig的配置,方便学习tsconfig配置项,mark一下。
执行ts文件的工具
ts-node 类似于执行js文件,可以使用node index.js命令,安装ts-node后,可以使用ts-node index.ts命令执行ts文件,前提是安装了typescript,并初始化tsconfig.json文件。
除了ts-node,也可以用node + require hook形式
node -r ts-node/register index.ts
ts-node命令行执行常用参数:
-P,--project 默认查找tsconfig.json配置文件,其他命名的配置文件用。-T, --transpileOnly 禁用掉执行过程中的类型检查过程--swc:在 transpileOnly 的基础上,还会使用 swc 来进行文件的编译,进一步提升执行速度。--emit: 需要查看ts编译产物用,,需要同时与--compilerHost选项一同使用
ts-node-dev 监听ts文件变更,并重新执行, 常用执行命令,respawn 用于监听重启
ts-node-dev --respawn --transpile-only app.ts
扩展 node require extension
node中提供了扩展导入的方法,除了js文件的导入,可以通过require extension导入自定义扩展名文件。 node.js的require的大致逻辑是
- 拼接绝对路径,后缀名不同,模块的解析策略不同
- 根据绝对路径,查找是否有缓存
- 实例化module类实例,根据后缀名调用内置处理函数。
- 根据文件类型,包装文件内容
- 执行文件内容
- 进行缓存
对上述逻辑进行拦截,可以进行ts文件编译、js文件babel编译等操作。
Day 2 Primitive and Object
原始类型的类型标注
JavaScript中的原始类型object、number、boolean、string、undefined、null、symbol、bigint都有对应的ts类型标注,上述类型基本都可以自然对应到ts中。
null & undefined
在typescript中,null和undefined都有具体的类型表示,在没有开启strictNullChecks配置的时候,两者表示为其他类型的子类型。在下面的例子中,关闭strictNullChecks配置的时候,string类型会被视作包含了null和undefined类型。
const x: string = null;
const y: string = undefined;
void
在js中,void 运算符对给定的表达式进行求值,然后返回 undefined。
在ts中,void表示一个空类型,如下函数的返回值类型都可以表示为void类型,都表示没有返回一个有意义的值。
function func1() {
}
function func2() {
console.log()
}
function func3() {
return undefined
}
数组
ts中大多使用const x: string[] = ['a','b','c']中对数组进行类型定义。
对于定长数组,可以使用元组(Tuple) 进行类型声明。
相对于数组进行类型声明,其优点如下:
- 对于数组越界,可以有明确的提示, 隐式越界也有相应的警告,并对不同位置的变量进行不同的类型声明
const list: [string, number, string] = ['a', 123, 'b']
list[4] // 此处访问会报错
- 具备可选类型声明,可选类型声明时,数组的长度属性也发生变化
const list: [string, number?, string?] = ['a', 123, 'b']
- 具名元组特性
const list: [name:string, age?: number, city?: string] = ['a', 123, 'b']
对象 + interface
使用interface为对象进行类型标注,有两个常见的修饰可选(Optional) 和 只读(Readonly)
interface Obj{
name:string;
readonly key:number;
content?:string;
}
const obj: Obj = {
name: 'init',
key: 0
}
obj.key = 1 // 此处对readonly修饰的key重新赋值就会报错
数组和元组的只读属性与对象只读修饰的区别:
- 对象可以对某个属性进行只读修饰,数组和元组只能标记整体为只读
- 对数组或元组标记为只读,即标记为
ReadOnlyArray,同时不再具备更改数组本身的方法,如push、pop等。
装箱类型
避免使用Object、Number、String等其他装箱类型,因其包含一些非预期的类型,如undefined、null、void。
Day 3 Literal and Enum 字面量类型与枚举
在日常开发过程中,类型定义的时候如果有确定的值或确定值的集合,可以使用字面量类型进行标注。字面量类型代表比原始类型更精确的类型,是原始类型的子类型。
interface Obj {
name: 'this' | 'that',
age: 18 | 28 | 38,
}
联合类型
联合类型是一组类型的可用集合,联合类型中可以嵌套联合类型,但最终会被展平到第一层级中。
比较有用的场景,手动实现互斥属性。
interface Template {
user:
| {
flag: true,
name: string,
age: number
} |
{
flag: false,
name: string,
age: number
}
}
枚举
enum PageType {
Home = 'home',
About = 'about',
Setting = 'setting'
}
未声明枚举类型的值时,会默认使用数字枚举,从0开始,步长为1,进行递增。如果中间的成员设置了枚举值,后面的成员则根据设置的枚举值开始递增。
enum Count{
Foo, // 0
Boo, // 1
Q = 4, // 4
W // 5
}
枚举可以进行双向映射,即从枚举成员到枚举值,从枚举值到枚举成员。从枚举的编译产物中可以看到,进行了两次赋值。
enum Count {
A,
B,
C,
D
}
Count[0] // 'A'
Count.A // 0
// 编译产物如下
var Count;
(function (Count) {
Count[Count["A"] = 0] = "A";
Count[Count["B"] = 1] = "B";
Count[Count["C"] = 2] = "C";
Count[Count["D"] = 3] = "D";
})(Count || (Count = {}));
const enum Num {
A,
B
}
const y = Num.A
// 编译产物如下
const y = 0 /* Num.A */;
Tips:
只有值为数字的枚举,可以进行双向映射Count["A"] = 0这样的表达式最终返回的是0
常量枚举,不能通过值去访问枚举成员,在编译上不会存在上述Count的辅助对象,对枚举成员的访问,会直接内联替换为枚举的值。
Day 4 Function and Class
函数重载
实际开发中,函数会有多种入参类型和返回值类型,这种情况下,可以使用函数重载签名。
function test(flag: true, content?: string): string;
function test(flag: false, content: string): number;
function test(flag: boolean, content?: string): string | number;
function test(flag: boolean, content?: string): string | number {
if (flag) {
return content || '';
}
return 123;
}
test(true, '123') // string
test(false, '123') // number
test(false) // string | number
有多种重载声明的函数,类型匹配时是按照声明顺序,向下查找的。
ts中的重载更多指的是伪重载,使用时更多体现在函数调用的签名中,而不是实际实现上,区别于Java、C++等语言。
Class
类中的类型声明,构造函数、属性、方法基本跟对应的变量类型声明、函数类型声明一致。
class PlayGround {
props: string;
constructor(input: string) {
this.props = input
}
speak(word: string): string {
return word
}
get prop(): string {
return this.prop
}
// set方法不允许对返回值类型进行标注
set prop(value){
this.prop = value
}
}
修饰符 public、private、readonly、protected
class PlayGround {
props: string;
constructor(input: string) {
this.props = input
}
public speak(word: string): string {
return word
}
protected reset() {
this.props = ''
}
private speakLoud(word: string): string {
return word.toUpperCase()
}
get prop(): string {
return this.prop
}
set prop(value) {
this.prop = value
}
}
- Public 可以在类、类的实例、子类中访问
- Private 只可以在类中访问
- protected 只可以在类、子类中访问
static 静态成员
类的静态成员无法通过this去访问,只能通过类型PlayGround.speakNothing访问。从编译结果看,静态成员被挂载在函数体上,无法通过原型链去访问到,同时也不会被实例继承。
class PlayGround {
props: string;
constructor(input: string) {
this.props = input
}
static speakNothing(){
return ''
}
get prop(): string {
return this.prop
}
set prop(value) {
this.prop = value
}
}
// 编译为ES5
var PlayGround = /** @class */ (function () {
function PlayGround(input) {
this.props = input;
}
PlayGround.speakNothing = function () {
return '';
};
Object.defineProperty(PlayGround.prototype, "prop", {
get: function () {
return this.prop;
},
set: function (value) {
this.prop = value;
},
enumerable: false,
configurable: true
});
return PlayGround;
}());
继承、抽象类
override关键字用来确保派生类中的方法一定存在于基类中,标识此方法会覆盖基类中的方法。
class myPlayGround extends PlayGround {
override speak(word: string): string {
return word.toLowerCase()
}
// 如果基类中没有相关方法声明,就会报错
override abc(){
}
}
抽象类用来描述类中应有的成员,包括属性、方法等,实现抽象类的时候,需要确保类中包含抽象类的每一个属性和方法。
abstract class Ground {
abstract props: string;
abstract speak(word: string): string;
}
class PlayGround implents Ground{
...
...
}
私有构造函数
通常,不会对构造函数前加修饰符;比如,在构造函数前添加private修饰符说明,在实例化的时候就会提示。
class privateCon {
private constructor(){
}
}
// Constructor of class 'privateCon' is private and only accessible within the class declaration.
const con = new privateCon()
使用场景:在不希望类被实例化使用(比如:utils工具类中,都是static成员),或者希望实例化逻辑通过方法来实现。
Day 5 Any & Unknown & Never
主要记录一下unkonwn类型和nerver类型。 unknown->未知类型,unknown类型标识的变量可以再次赋值为其他类型,但只能赋值给any或unknown类型的变量。
let x:unknown = '1'
x = '2'
// Type 'unknown' is not assignable to type 'string'
let y: string = x
let obj:unknown = {
}
// 区别于any类型,unknown类型在属性访问时依旧会进行检查
// 'obj' is of type 'unknown'.
obj.x
never类型
never 类型不携带任何的类型信息,是整个类型系统层级中最底层的类型
类型层级
后续会详细介绍类型层级内容
- 最顶级的类型,any 与 unknown
- 特殊的 Object ,它也包含了所有的类型,但和 Top Type 比还是差了一层
- String、Boolean、Number 这些装箱类型
- 原始类型与对象类型
- 字面量类型,即更精确的原始类型与对象类型,需要注意的是 null 和 undefined 并不是字面量类型的子类型
- 最底层的 never
Day 6 Internal Type Tools
type 类型别名
// 一般使用
type x = string;
type obj = {
name:string;
age:number
}
// 作为工具类型使用
type Factory<T> = T | string | number;
交叉类型 &
interface Name {
name: string;
}
interface Age {
age: number
}
type Person = Name & Age
const person: Person = {
name: '1',
age: 1
}
// 交叉类型需要同时满足两个对象的结构
// 否则就会报错
// Property 'age' is missing in type '{ name: string; }' but required in type 'Age'
const otherPerson: Person = {
name: '234'
}
// 字面量类型合并的时候,会变成never
// 因为没有一个类型能同时满足string和number类型,即不存在的类型never
type combine = string & number'
// 联合类型进行交叉类型合并时,会取交集。
type mix = (string | number) & (string | boolean) // string
type mixAgain = (1 | 2 | 3) & (3 | '1') // 3
索引签名类型
interface IndexList {
[key: string]: string;
}
const list: IndexList = {
'a': '1F',
1: '1',
[Symbol(1)]: '1'
}
typeof 类型查询操作符
const str = "linbudu";
const obj = { name: "linbudu" };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
return input.length > 10;
}
type Str = typeof str; // "linbudu"
type Obj = typeof obj; // { name: string; }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean
类型守卫 xxx is 某个类型
// 类似于类型断言,通过使用 str is string 如果函数返回true,标记str为string类型
function judgeString(str: unknown): str is string{
return typeof str === 'string'
}
Day 7 Generic Types
类型别名中使用泛型
// 类似于一个接受参数的函数,泛型类似于函数的参数,返回值为类型
type Anything<T> = T
const str:Anythins<string> = '123'
//通过泛型实现工具类的封装
// 成员内的类型都为字符串类型
type Stringify<T> = {
[K in keyof T]: string;
}
// 将成员变成可选项
type Partial<T> = {
[k in keyof T]?: T[k]
}
// 通过extends 实现条件判断
type IsEqual<T> = T extends string ? 'Y' : 'N'
type A = IsEqual<1> // 'N
type B = IsEqual<'1'> // 'Y'
// 支持默认值
type DefaultNumber<T extends number = 1> = T | 2 | 3;
const num: DefaultNumber = 2
多泛型关联
// 可以同时传入多个泛型参数,同时多个泛型之间也存在着关联
type PInput<Input, SecondInput extends Input = Input, ThirdInput extends Input = SecondInput> = number
const x: PInput<number> = 123
对象类型中的泛型
// 类似于值的传递
interface ReturnData<T> {
code: number,
message: string,
data: T
}
interface SomeList {
key: number,
name: string,
content?: string
}
function fetchSomeThing(): Promise<ReturnData<SomeList>> {} // 此处未返回数据,会不通过,只是演示
类型自动提取
// 返回值的类型,会自动根据实际传入的类型做推断,
function swap<T, U>([start, end]: [T, U]): [U, T] {
return [end, start];
}
const swap1 = swap(['1', 2])
const swap2 = swap([0, true])
Day 8 Structural Type System 结构化类型系统
class Dog {
eat()
}
class Cat {
eat()
}
function showSomething(cat: Cat) {
cat.eat()
}
// showSomething函数中传入了Dog类,并没有报错
showSomething(new Dog())
结构化类型系统,别称鸭子系统,根据类型中的结构、属性来判断是否为同一种类型,上面的cat和dog类中,都只有一个eat方法,所以会判断为同一个类型。
鸭子类型:James Whitcomb Riley提出,表述为“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
class Dog {
eat()
}
class Cat {
eat()
fish()
}
function showSomething(cat: Cat) {
cat.eat()
}
// 在cat类中加入独有的方法,就会报错
// 'fish' is declared here.
showSomething(new Dog())
标称类型系统
区别于结构化类型,标称类型系统需要,两个可兼容的类型,其名称必须是完全一致的
type USD = number;
type CNY = number;
const cny: CNY = 200
const usd: USD = 200;
function add(first: CNY, sec: CNY) {
return first + sec
}
// ts中,结构化类型结构对于这种情况,无法做出识别
// 无报错
add(cny, usd)
在ts中模拟标称类型系统
类型的重要意义之一是限制了数据的可用操作与实际意义待理解
// 通过绑定独有的元数据__tag,使ts判断的时两个类型表现为不兼容
declare class TagProto<T extends string>{
protected __tag: T
}
type NormalMix<T, U extends string> = T & TagProto<U>
type USD = NormalMix<number, 'USD'>
type CNY = NormalMix<number, 'CNY'>
const cny = 100 as CNY
const usd = 200 as USD
function add(first: CNY, sec: CNY) {
return first + sec
}
add(cny, usd)
Day 9 Type Levels 类型系统层级
总的来说,可以用这张图来概括。
判断类型兼容性
// 通过extends关键字判断类型兼容
type res = '123' extends string ? 1 : 2 // 1
// 通过赋值判断
declare let source: string;
declare let anyType: any;
declare let unkonwnType: unknown
declare let neverType: never
anyType = source
unkonwnType = source
// error: Type 'string' is not assignable to type 'never'.
neverType = source
了解类型层级间的兼容性关系
type str = '123' extends string ? 1 : 2 // 1
type num = 123 extends number ? 1 : 2 // 1
type obj = { a: 1 } extends object ? 1 : 2 // 1
// 这里注意:object代表着所有非原始类型的类型,即对象、数组和函数。
// [] 数组可以被认为使object的字面量类型
type emptyArr = [] extends object ? 1 : 2 // 1
关于object和Object、{}的关系有点复杂。
// string是String的字面量类型
type res1 = string extends String ? 1 : 2 // 1
// String可以看作是普通的对象,只是里面包含了字符串的一些方法,所以{}兼容String
type res2 = String extends {} ? 1 : 2 // 1
// 如下
interface String{
splice,
split,
...
}
// {}为object、Object的字面量类型
type res1 = {} extends object ? 1 : 2; // 1
type res6 = {} extends Object ? 1 : 2; // 1
// 从结构化类型系统中比较,{}可以被看作为所有类型的基类,
// 因为它没有属性,其他对象可以看作继承于它,之后实现自己内部的方法
type res2 = object extends {} ? 1 : 2; // 1
type res5 = Object extends {} ? 1 : 2; // 1
// 属于'系统设定'原因
// Object 包含了所有除 Top Type 以外的类型(基础类型、函数类型等)
// object 包含了所有非原始类型的类型,即数组、对象与函数类型,这就导致了你中有我、我中有你的神奇现象。(不太理解)
type res3 = object extends Object ? 1 : 2; // 1
type res4 = Object extends object ? 1 : 2; // 1
any、unknown类型
// 作为top level,兼容性最强
type res = Object extends any ? 1 : 2; // 1
type res1 = Object extends unknown ? 1 : 2; // 1
// '系统设定'原因,当any作为判断条件,即返回判断类型结果的联合类型
type res2 = any extends Object ? 1 : 2; // 1 | 2
// unknown只允许被赋值给any、unknown类型
type res3 = unknown extends Object ? 1 : 2; // 2
nerver类型
// never类型小于字面量类型
type neverType = never extends '1' ? 1 : 2 // 1
Day10 Conditional Types 条件类型
条件类型
// 条件类型可以针对泛型类型做进一步的类型推断等类型操作
// 条件类型这里的类似于类型判断的实际逻辑
type Middler<T extends string | number> = T extends string ? string : T extends number ? number : nerver
// 函数命名这里的泛型约束类似于函数入参校验
function universe<T extends string | number>(x: T, y: T): Middler<T> {
return x + (y as any)
}
universe(123, 1) // number
universe('123', '1') // string
infer 关键字
通过infer关键字,可以在条件类型中提取类型信息
// infer关键字 提取函数返回的类型,在类型判断中,使用推断后的类型作为判断逻辑的一部分。
type FuncReturn<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;
type x = FuncReturn<() => string> // string
type y = FuncReturn<() => number> // number
type z = FuncReturn<() => boolean> // boolean
// 根据key获取对象的value值
type ExtractPropValue<T, K extends keyof T> = T extends { [Key in K]: infer R } ? R : never
type value = ExtractPropValue<{ name: 1, num: '213' }, 'name' | 'num'> // boolean | 1 | '213'
// infer使用 会有两种场景出现类型丢失
// 1.键值类型
// 类型丢失:Type 'K' does not satisfy the constraint 'string | number | symbol'.
type Reverse<T extends Record<string, string>>
= T extends Record<infer U, infer K> ? Record<K, U> : never
// Fix with 交叉类型 保证K的类型为string(感觉只是兜底方案?)
type Reverse<T extends Record<string, string>>
= T extends Record<infer U, infer K> ? Record<K & string, U> : never
// 2.嵌套结构
type PromiseType<T> = T extends Promise<infer R> ? R : T;
type PromiseInside<T> = T extends PromiseType<infer R> ? R : T;
type PP = PromiseType<Promise<boolean>> // boolean
type PPPP = PromiseInside<Promise<Promise<boolean>>> // Promise<Promise<boolean>> 未进行类型解析
// 使用嵌套提取的方式,甚至可以使用递归的方法
type PromiseRecursion<T> = T extends Promise<infer R> ? PromiseRecursion<R> : T
type PromiseFor<T> = T extends Promise<infer R> ? R extends Promise<infer D> ? D : R : T
type PPP = PromiseRecursion<Promise<Promise<string>>> // string
type PF = PromiseFor<Promise<Promise<string>>> // string
条件类型的分布式特性
对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上。
type Condition<T> = T extends 1 | 2 | 3 | 4 | 5 ? T : never
// 此处传入1 | 2 | 6,返回的类型为联合类型分拆开后分别执行extends操作判断,之后合并的结果
type condition = Condition<1 | 2 | 6> // 1 | 2
// 通过包裹参数,可以禁用分布式特性
type Wrapper<T> = [T] extends [1,2,3,4,5] ? T :never
type wrapper = Wrapper<1 | 2 | 6> // never
Day11 Builtin Tool Types 工具类型
属性修饰类型
// 可选类型
type Partial<T> = {
[P in keyof T]?: T[P]
}
// -? 表示去除可选标记, 类似 +? 表示添加可选标记
type Required<T> = {
[P in keyof T]-?: T[P]
}
// 只读类型
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
结构工具类型
// 从T中挑选出传入的键值
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
// 相反,从T中排除传入的键值
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
集合工具类型
// T、K都为联合类型时,extends计算会将T中的每个类型进行计算,最终返回每个计算合并后的联合类型
// 交集
type Extract<T, K> = T extends K ? T : never;
type x = Extract<1 | 2 | 3, 1 | 5 | 3> // 1 | 3
// Extract中的计算可以用这种方式来表示
type xa =
(1 extends 1 | 5 | 3 ? 1 : never) |
(2 extends 1 | 5 | 3 ? 2 : never) |
(3 extends 1 | 5 | 3 ? 3 : never)
// 差集
type Exclude<T, K> = T extends K ? never : T;
type y = Exclude<1 | 2 | 3, 1 | 4> // 2 | 3
// 并集
type Union<T, K> = T | K
// 补集
type Complement<T, K extends T> = Exclude<T, K>
infer约束
// 提取数组的第一个元素
type ArrayFirstItem<T extends any[]> = T extends [infer P, ...any[]] ? P : never;
type ArrayFirstStringItem<T extends any[]> = T extends [infer P extends string, ... any[]] ? P : never;
type Arr2 = ArrayFirstStringItem<['321', 3321321]> // '321'
Day12 Contextual Typings 上下文类型
Day13 Covariance and Contravariance 协变与逆变
针对函数类型的签名类型,实际比较的是参数类型和返回值类型,来确定函数类型的类型层级。
协变与逆变的定义:随着某一个量的变化,随之变化一致的即称为协变,而变化相反的即称为逆变。
同步到ts中,假设A≤B,协变即指Wrapper<A> ≤ Wrapper<B>,逆变即指Wrapper<B> ≤ Wrapper<A>。
在tsconfig的配置中,strictFunctionTypes配置项就是指 在比较两个函数类型是否兼容时,将对函数参数进行更严格的检查,即开启逆变检查。
class Animal {
asPet() { }
}
class Dog extends Animal {
eat() { }
}
class Corgi extends Dog {
run() { }
}
function func(dog: Dog) {
dog.eat()
}
type CorgiFunc = (input: Corgi) => void;
type AnimalFunc = (input: Animal) => void
const func1: CorgiFunc = func;
// 开启了strictFunctionTypes配置,此处的赋值会报错
// Type '(dog: Dog) => void' is not assignable to type 'AnimalFunc'.
// 因为animal > dog不满足逆变条件
const func2: AnimalFunc = func;
关闭了strictFunctionTypes配置,上面的报错会消失,因为在默认条件下,对函数参数的检查是采用的双变(逆变|协变)
t中,只有通过property声明的方式,才可以在开启strictFunctionTypes配置时,对参数进行逆变检查。 对method声明的方式,采用双变检查。
// method 声明
interface method {
func(arg: string): number;
}
// property 声明
interface property {
func: (arg: string) => number;
}
Day14
Type Challenge
可以用来进阶类型编程能力的工具,mark。
Day15 工具类型进阶Advanced Builtin Tool Types
tsd 工具类型单元测试库
import { expectType } from 'tsd';
type DeepPartialStruct = DeepPartial<{
foo: string;
nested: {
nestedFoo: string;
nestedBar: {
nestedBarFoo: string;
};
};
}>;
// 使用expectType来验证传入的参数是否符合所定义的类型
expectType<DeepPartialStruct>({
foo: 'bar',
nested: {},
});
属性修饰进阶
// 使用递归进行深层属性修饰
type DeepPartial<T extends object> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type DeepRequired<T extends object> = {
[K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};
type DeepReadonly<T extends object> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type DeepMutable<T extends object> = {
-readonly [K in keyof T]: T[K] extends object ? DeepMutable<T[K]> : T[K];
};
type NonNullable<T> = T extends null | undefined ? never : T;
type DeepNonNullable<T extends object> = {
[K in keyof T]: T[K] extends object ? DeepNonNullable<T[K]> : NonNullable<T[K]>;
};
实现复杂的工具类型,可以将其拆解为由基础工具类型、类型工具的组合。
// 标记对象中指定的属性为可选
// 通过 拆分对象结构 + 属性修饰 + 类型组合 完成复杂的工具类型
export type MarkPropsAsOptional<
T extends object,
K extends keyof T = keyof T
> = Partial<Pick<T, K>> & Omit<T, K>;
结构工具类型
基于键值类型的Pick、Omit
// 基于期望的类型获取对于该类型的属性值
type FuncStruct = (...args: any[]) => any;
// 获取类型为函数类型的属性值
type FunctionKeys<T extends object> = {
[K in keyof T]: T[K] extends FuncStruct ? K : never;
}[keyof T];
// {...}[keyof T]语法 会对对象的key值逐个访问,然后形成联合类型
type Res = { foo: 'foo'; bar: 'bar'; baz: never; };
Res[keyof Res] -> "foo" | "bar" | never
// 基于结构的互斥工具类型
// 使用场景比如,各个子类拥有独特的属性,声明的时候要求子类中的独特属性不能同时拥有。
// 实现思路是,标记不能拥有的属性为never
// 将不存在于T中,存在于U中的属性标记为never
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
// 标记为never后,合并,这样类型声明的时候,会有互斥的属性标记为了never
type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T);
// 使用XOR实现 foo和bar属性为绑定状态,要么为空,要不两个属性同时存在
type XORStruct = XOR< {}, { foo: string; bar: number; } >;
集合工具类型
// 对集合工具类型进行交并补差集运算,可以降级为对集合内的属性名进行相应计算
// 使用更精确的对象类型描述结构
type PlainObjectType = Record<string, any>;
// 属性名并集
type ObjectKeysConcurrence< T extends PlainObjectType, U extends PlainObjectType > =
keyof T | keyof U;
// 属性名交集
type ObjectKeysIntersection< T extends PlainObjectType, U extends PlainObjectType > =
Intersection<keyof T, keyof U>;
// 属性名差集
type ObjectKeysDifference< T extends PlainObjectType, U extends PlainObjectType > =
Difference<keyof T, keyof U>;
// 属性名补集
type ObjectKeysComplement< T extends U, U extends PlainObjectType > =
Complement<keyof T, keyof U>;
// 获取到对应的属性名,之后就可以用pick拿到键值对,进行后续的操作
模式匹配工具类型
// 深层嵌套 infer
type FirstParameter<T extends FunctionType> =
T extends ( arg: infer P, ...args: any ) => any ? P : never;
// 提取返回值类型
// 通过infer,修改类型结构判断,提取想要的类型
type Awaited<T> =
T extends null | undefined ?
T : T extends object & { then(onfulfilled: infer F): any } ?
F extends (value: infer V, ...args: any) => any ?
Awaited<V> : never : T;
扩展
// 如何获取可选或者只读的属性(实现思路比较重要,替换任务,使用辅助判断)
// 可选 -> 将获取可选属性任务 替换为 对象类型兼容性判断
type temp = {} extends { prop: number } ? "Y" : "N"; // "N"
type temp2 = {} extends { prop?: number } ? "Y" : "N"; // "Y"
type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K; }[keyof T];
// 只读 -> 判断类型结构的全等性,全等性判断包含了只读和可选等属性
type Equal<X, Y, A = X, B = never> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
type MutableKeys<T extends object> =
{ [P in keyof T]-?:
Equal< { [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never >; }[keyof T];