whinc / whinc.github.io

个人站点,包含技术笔记、博客、作品、工具等

Home Page:https://whinc.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TypeScript 技巧集锦

whinc opened this issue · comments

原文链接:#18

image

编写 TypeScript(后面简称TS)应用是一个与类型斗争的过程,你需要使用 TS 提供的类型工具通过不同的组合来精确描述你的目标。描述越精确,类型约束和提示越准确,潜在错误越少。反之,描述越模糊(如any一把唆),TS 能提供的类型辅助就越少,潜在的错误也就越多。如何描写精确的类型描述需要掌握 TS 的基础概念,同时掌握常见技巧和类型工具,前者可以阅读官网的 TypeScript 手册 学习,后者可以通过本文学习一二,后续进阶学习就靠多实践多总结了。

常用技巧

这节主要介绍一些基础的类型工具,这是所有高级类型的基石。

typeof T - 获取 JS 值的类型

typeof可以获取 JS 变量的类型,它是 JS 值空间向 TS 类型空间转换的桥梁,有了它我们可以从已有的变量中抽取类型进行进一步处理。

const person = {
    name: "jim",
    age: 99
}
 
type Person = typeof person
// type Person = {
//     name: string;
//     age: number;
// }

keyof T - 获取类型的键

keyof可获取目标类型的键,返回的是string | number | symbol的子类型。

interface Person {
    name: string
    age: number
}
 
type K = keyof Person
// type K = "name" | "age"

进一步阅读:

T[K] - 索引类型,获取类型的值

动态获取目标类型属性的类型,类似 JS 中对象取值操作,不过这里取到的是值的类型。

interface Person {
    name: string
    age: number
}
 
type T1 = Person['name'] // string
type T2 = Person['age'] // number
type T3 = Person[keyof Person] // string | number

进一步阅读

[P in keyof T]: T[P] - 类型映射,转换类型

基于旧类型创建新类型,在新类型构造过程中,我们可以对旧类型的属性名、属性值进行重写,从而实现类型转换。其中in操作符表示遍历目标类型的 key。

interface Person {
    name: string
    age: number
}
 
// 将 Person 的属性转换成可选
type PersonPartical = { [P in keyof Person]?: Person[P] }
// type PersonPartical = {
//     name?: string | undefined;
//     age?: number | undefined;
// }

// 将 Person 的属性转换成只读
type PersonReadonly = { readonly [P in keyof Person]: Person[P] }
// type PersonReadonly = {
//     readonly name: string;
//     readonly age: number;
// }

// 上面了两个操作实在太常用了,TS 已经内置了相应的类型工具
// 例如上面的可选和只读,可以写成
type PersonPartical = Partial<Person>
type PersonReadonly = Readonly<Person>

进一步阅读

T extends U ? X : Y - 条件类型

extends除了用在继承类时会使用,还可以用于判断一个类型是否是另一个类型的父类型,并根据判断结果执行不同的类型分支,其使得 TS 类型具备了一定的编程能力。

extends条件判断规则如下:如果T可以赋值给U返回X,否则Y,如果 TS 无法确定T是否可以赋值给U,则返回X | Y

type isString<T> = T extends string ? true : false
 
type T1 = isString<number>  // false
type T2 = isString<string>  // true

进一步阅读

infer T - 类型推断

extends条件类型的子句中,可以使用infer T来捕获指定位置的类型(该类型由 TS 编译器推断),在infer后面的子句中可以使用捕获的类型变量。配合extends条件类型,截取符合条件的目标的某部分类型。

type ParseInt = (n: string) => number
// 如果是类型 T 是函数,则 R 会捕获其返回值类型并返回 R,否则返回 any
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
type R = ReturnType<ParseInt>   // number
 
type GetType<T> = T extends (infer E)[] ? E : never
type E = GetType<['a', 100]>

进一步阅读

never

never与类型TT是除unknown外的其他任意类型)union 后结果是类型T,利用never的这个特点可以实现类型消除,例如将某个类型先转换成never,然后再与其他类型 union。

type a = string
type b = number
type c = never
type d = a | b |c
// type d = string | number
 
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<string | number, string>   // number

进一步阅读

类型工具

TS 内置了一些常用的类型转换工具,熟练掌握这些工具类型不仅可以简化类型定义,而且可以基于此构建更复杂的类型转换。

下面是 TS 内置的所有类型工具,我加了下注释和示例方便理解,你可以先只看示例,测试下能否自行写出对应的类型实现(Playground)。

/**
* 使 T 的所有属性变为为可选的
*
* Partial<{name: string}> // {name?: string | undefined}
*/
type Partial<T> = {
    [P in keyof T]?: T[P];
};
 
/**
* 使类型 T 的所有属性变为必需的
*
* Required<{name?: string}> // {name: string}
*/
type Required<T> = {
    [P in keyof T]-?: T[P];
};
 
/**
* 使类型 T 的所有属性变为只读的
*
* Readonly<{name: string}>  // {readonly name: string}
*/
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
 
/**
* 从类型 T 中挑出所有属性名出现在类型 K 中的属性
*
* Pick<{name: string, age: number}, 'age'>  // {age: number}
*/
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
 
/**
* 构造一个 key-value 类型,其 key 是类型 K, value 是类型 T
*
* const map: Record<string, number> = {a: 1, b: 2}
*/
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
 
/**
* 从类型 T 中剔除类型 U
*
* Exclude<'a' | 'b', 'a'>   // 'b'
*/
type Exclude<T, U> = T extends U ? never : T;
 
/**
* 从类型 T 中挑出类型 U
* 
* Extract<string | number, number> // number
*/
type Extract<T, U> = T extends U ? T : never;
 
/**
* 从类型 T 中剔除所有属性名出现在类型 K 中的属性
*
* Omit<{name: string, age: number}, 'age'>  // {name: number}
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
 
/**
* 剔除类型 T 中的 null 和 undefined 子类型
*
* NonNullable<string | null | undefined> // string
*/
type NonNullable<T> = T extends null | undefined ? never : T;
 
/**
* 获取函数的参数元组(注意是元组不是数组)
* 
* Parameters<(name: string, age: number) => void> // [string, number]
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
 
/**
* 获取构造函数的参数元组
*
* class Person { constructor(name: string, age: number) { } }
* ConstructorParameters<typeof Person> // [string, number]
* 
* TS 中类有两个方面:实例面、静态面
* typeof Person 表示类的静态面类型
* Person 表示类的静态面实例,如构造函数、静态方法
* Person 也表示类实例的类型,如成员变量、成员方法
* new Person 表示类的实例
*/
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
 
/**
* 获取函数的返回类型
*
* ReturnType<() => string> // string
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
 
/**
* 获取构造函数的返回类型,即类的实例的类型
*
* class Person { constructor(name: string, age: number) { } }
* InstanceType<typeof Person> // Person
*/
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

除了 TS 内置的类型工具外,还有一些第三方开发的类型工具,提供了更多类型转换工具,例如 ts-toolbelt —— TS 版"lodash"库。

下面是从 ts-toolbelt 挑选的部分示例,更多工具类型请查看它的官网。

import type { Object } from 'ts-toolbelt'

// 使对象的部分属性变为可选
type T1 = Object.Optional<{ a: string; b: number }, 'a'>
// type T1 = {
//     a?: string | undefined;
//     b: number;
// }

// 合并两个对象,前面对象为 undefined 的属性被后面对象对应属性覆盖
type T2 = Object.MergeUp<{ a: 'a1', b?: 'b1' }, { a: 'a2', b: 'b2' }>
// type T2 = {
//     a: "a1";
//     b: "b1" | "b2";
// }

案例解析

掌握基础概念后,可能依然无法写出精确的类型描述,因为这些概念仅仅停留在单个概念的使用,需要进一步实践练习,才可能融会贯通。下面搜集了一些 TS 的类型转换案例(题目),可以从中学习一些解题思路和代码实现。

No.1

问题

假定对象的所有值都是数组类型,例如:

const data = {
  a: ['x', 'y', 'z'],
  b: [1, 2, 3]
} as const

要求获取上述对象值中的数组元素的类型,例如:

type TElement = "x" | "y" | "z" | 3 | 1 | 2

解题思路

首先拿到对象的值类型,然后通过数组下标获取数组元素的类型。

参考代码

type GetValueElementType<T extends { [key: string]: ReadonlyArray<any> }> = T[keyof T][number]
type TElement  = GetValueElementType<typeof data>

扩展

如果对象的值不都是数组类型呢?

例如下面这样

const data = {
  a: ['x', 'y', 'z'],
  b: [1, 2, 3],
  c: 100
} as const

解题思路:首先依然是拿到对象的值类型,然后过滤出数组类型,最后取数组的元素类型

// 实现1:通过 extends 判断对象的值类型,通过数组下标获取元素类型
type GetValueElementType<T> = { [K in keyof T]: T[K] extends ReadonlyArray<any> ? T[K][number] : never }[keyof T]
 
 
// 实现2:通过 extends 判断对象的值类型,通过 infer 推断,获取数组元素类型
type GetValueElementType<T> = { [K in keyof T]: T[K] extends ReadonlyArray<infer E> ? E : never }[keyof T]

No.2

问题

假设有一个EffectModule类,它包含成员变量和成员方法,代码如下:

interface Action<T> {
  payload?: T;
  type: string;
}

class EffectModule {
  count = 1;
  message = "hello!";

  delay(input: Promise<number>) {
    return input.then(i => ({
      payload: `hello ${i}!`,
      type: 'delay'
    }));
  }

  setMessage(action: Action<Date>) {
    return {
      payload: action.payload!.getMilliseconds(),
      type: "set-message"
    };
  }
}

现在有一个叫connect的函数,它接受EffectModule实例,将它变成另一个对象,这个对象上只有EffectModule的同名方法,

type Connected = {
  delay(input: number): Action<string>
  setMessage(action: Date): Action<number>
}
const effectModule = new EffectModule()
const connected: Connected = connect(effectModule)

即经过connect函数后,方法的类型签名变成了:

asyncMethod<U, R>(input: Promise<U>): Promise<Action<R>>  
// 变成
asyncMethod<U, R>(input: U): Action<R> 

syncMethod<U, R>(action: Action<U>): Action<R>
// 变成
syncMethod<U, R>(action: U): Action<R>

要求实现下面的Connect函数类型,将any替换成题目的解答后,让编译器可以顺利编译通过,并且返回的类型与Connected相同。

type Connect = (module: EffectModule) => any

本题来自LeeCode**区招聘的一道面试题.

解题思路

  1. 过滤出EffectModule实例的成员方法
  2. 通过T extends U判断方法签名
    a. 如果方法签名符合条件,使用infer捕获 Promise 和 Action 中的泛型参数,并返回正确的方法类型签名
    b. 否则,返回方法原始的类型签名

参考代码

// 获取对象中value为函数的属性名称
type FilterFunctionNames<T extends {}> = {[P in keyof T]: T[P] extends Function ? P: never}[keyof T]
// 转换函数类型签名
type TransformFunctions<T extends {}> = {
    [P in keyof T]: T[P] extends (arg: Promise<infer U>) => Promise<Action<infer R>>
    ? (arg: U) => Action<R>
    : T[P] extends (arg1: Action<infer U>) => Action<infer R>
    ? (arg: U) => Action<R>
    : never
}
// 1. 过滤出 value 为函数类型的实例属性名称
// 2. 通过 Pick 挑出 value 为函数类型的成员组成新对象
// 3. 遍历对象的 key/value,将符合条件的类型签名转换成目标签名
type Connect = (module: EffectModule) => TransformFunctions<Pick<EffectModule, FilterFunctionNames<EffectModule>>>

上面的TransformFunctions的实现也可以简单点写,例如将返回值类型Promise<Action<infer R>>改成Promise<infer R>,却别在于前者判断时更加精准。前者约束返回值必须是 Promise + Action 类型,而后者只约束返回值是 Promise 类型。

type TransformFunctions<T extends {}> = {
    [P in keyof T]: T[P] extends (arg: Promise<infer U>) => Promise<infer R>
    ? (arg: U) => R
    : T[P] extends (arg1: Action<infer U>) => infer R
    ? (arg: U) => R
    : never
}

案例部分未完待续。。。

参考

whinc/ideas#1