TypeScript进阶学习笔记之一

353 阅读21分钟

这里是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", {
        getfunction () {
            return this.prop;
        },
        setfunction (value) {
            this.prop = value;
        },
        enumerablefalse,
        configurabletrue
    });
    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 类型系统层级

总的来说,可以用这张图来概括。 image.png

判断类型兼容性

// 通过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];