Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript 技巧集锦 #20

Open
whinc opened this issue Apr 29, 2020 · 1 comment
Open

TypeScript 技巧集锦 #20

whinc opened this issue Apr 29, 2020 · 1 comment
Labels

Comments

@whinc
Copy link
Owner

whinc commented Apr 29, 2020

原文链接:#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
Copy link
Owner Author

whinc commented May 18, 2020

whinc/ideas#1

@whinc whinc added JS/TS and removed TypeScript labels Nov 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant