复杂场景下的 typescript 类型锚定 (2) ----- 泛型与类型推导

前言:在编写 typescript 应用的时候,有时候咱们会但愿复用或者构造一些特定结构的类型,这些类型只从 typescript 靠内建类型和 interface、class 比较难以表达,这时候咱们就须要用到类型推导, 而讨论类型推导, 则离不开泛型推断(#infer), 本文咱们只讨论泛型html

上一篇typescript

泛型

从形式上看, typescript 中的泛型如同大多数语言(不包括还没有实现的 Go :P)里的泛型:数组

// one constrain for array-like object
interface ArrayLike<T> {
    readonly length: number;
    readonly [n: number]: T;
}复制代码

上面的 ArrayLike 表达了类数组结构, 它表明的对象的特征是:bash

  1. 有只读的 length 字段;
  2. 其它字段必须是数字, 且每一个字段对应的值类型必须为 T, 这里的 T 就是泛型标记

array-like 对象是 Javascript 中很古老的对象, 如声明为 function (...) {...} 格式的函数, 其内部的 arguments 变量就是典型的 array-like 对象.函数


泛型每每能够用在这些场景:学习

  • 保留字 interface
  • 保留字 type
  • 命名空间(#namespace) 中的 class(实际上此时它是一个 interface)
  • 运行时中的 class
  • 函数定义(包括 class 的构造函数)

接下来咱们依次说明, 在这些场景中, 类型推导如何发挥它的威力.ui


基于泛型的类型推导


在利用泛型作类型推导时, 切记:es5


  1. 泛型服务于类型的静态分析, 不服务于 Javascript 运行时
  2. 在考虑类型推导时的逻辑推算, 应考虑"它在运行时会获得何种类型", 而应考虑"基于类型自己的特性会获得何种类型"


第 2 点可能有点难以理解, 咱们先略过, 在下一篇, 咱们会明白这句话的含义.spa


interface: T 与 keyof


全键可选化


上一篇层提到, 咱们能够经过 keyof 提取一个 interface 的全部键名, 当引入泛型后, keyof 还能够作更有趣的事情:code


/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};复制代码


Partial 的做用是: 对 T(T 须要是可被当作 interface 的类型), 求其全部的键名, 并依赖键名, 获得一个结构彻底相同, 但其全部键均可选 的新 interface.


好比, 对与 Form, UninitForm 能够是它的全键可选项版本:


interface Form {
    name: string
    age: number
    sex: 'male' | 'female' | 'other'
}

type UninitForm = Partial<Form>复制代码


则 UninitForm 等价于:


{
    name?: string;
    age?: number;
    sex?: "male" | "female" | "other";
}复制代码


全键必需化


相反, 若是你已经有了 UninitForm, 则你能够得靠 Required 到它的全键必需化版本:


/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};复制代码


interface UninitForm {
    name?: string;
    age?: number;
    sex?: "male" | "female" | "other";
}

type AllKeyRequriedForm = Required<UninitForm>复制代码


全键只读化


结合 readonly, 咱们能够把一个 interface 里全部的键转化为只读


/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};复制代码


强大的 extends 三元推导


对于 js 而言, extends 是扩展类的保留字; 而在 typescript 中, 当 extends 出现的以下的场景时, 它意味着类型推导:


/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;复制代码


形如 T extends [DEP] ? [RESULT1] : [RESULT2] 的表达式, 是 typescript 中的一种类型推导式, 它的规则是:


若泛型 T 必须知足 [DEP] 的约束(即 T extends [DEP]true), 则表达式结果为 [RESULT1]; 反之表达式结果为 [RESULT2]:


  1. 当 [DEP] 是基本类型时, 若是 T 是对应的基本类型, 则 T extends [DEP]true, 反之为 false
  2. 当 [DEP] 是 interface/class 时, 若是 T 必须知足它的约束, 则 T extends [DEP]true, 反之为 false
  3. 当 [DEP] 是 void/never 时, 按基本类型处理
  4. 当 [DEP] 是 联合类型, 组成 [DEP] 的类型会依次代入 T 进行运算, 最终的结果是这些运算结果的联合类型
  5. 当 [DEP] 是 any, 则 T extends [DEP] 恒为 true


按照这些规则, 咱们来分析一下这个 Exclude.


/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;复制代码


分析可知:


  • Exclude 中有两个必要的泛型标记 TU(由于它们都未提供默认泛型)
  • 若是 T 是联合类型, 则咱们会获得 T 中除了 U 以外的全部类型


咱们来应用一下 Exclude


type NoOne = Exclude<1 | 2 | 3, 1>  // NoOne = 2 | 3复制代码


另外, 为了说明上面的第 4 点, 咱们能够写一个毫无用处的 NotRealExclude;


type NotRealExclude<T, U> = T extends U ? U : T;

type Orig = NotRealExclude<1 | 2 | 3, 1> // Orig = 1 | 2 | 3复制代码


因为 NotRealExclude 在 T 不符合 U 的时候返回 U, 而在 T 符合 U 的时候又返回 T, 最终的结果是: 组成 T 的全部类型又被从新组装了起来.


extends 乱炖


在了解了 extends 的基本用法后, 咱们来看更多的例子:


挑选, 排除, 重组对象的键值


/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};复制代码


正如咱们经常使用的 pick 函数 能够提取对象中特定的键值对, Pick 也能够提取 T 中特定的键值定义.


interface Person {
    name: string
    age: number
    sex?: "male" | "female" | "other";
}

/**
 * equivalent to { name: string }
 */
type SimplePersonInfo = Pick<Person, 'name'>复制代码


既然能够 Pick, 那也能够 Omit


/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;复制代码


/**
 * equivalent to { name: string, age: number }
 */
type SimplePersonInfo = Omit<Person, 'sex'>复制代码


咱们知道, 在对象的索引中, in 关键字能够用于从联合类型中提取类型, 做为 interface 的键名, 好比:


type Ks = 'a' | 'b' | 'c'
type KObject = {
    [P in Ks]: any
}复制代码


则 KObject 能够包含 'a', 'b', 'c' 三种类型的键名.


结合 extends, 咱们能够轻松地从一个 interface 构建具备一样类型的值的字典, 这就是 Record:


/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};复制代码


// construct one family with 3 keys: parent, mom, child
type Family = Record<'parent' | 'mom' | 'child', Person>复制代码


这等价于


interface Family {
    parent: Person,
    mom: Person,
    child: Person
}复制代码


非 Null 化变量等


/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;复制代码


总结


至此,


  1. 咱们知道了如何利用泛型推出新的类型
  2. 咱们知道了如何将泛型与 extends 三元组, 索引 in 表达式 结合, 对 interface 进行拆解、重组


但到目前为止, 咱们处理的场景都限与非函数的 interface(class)/type, 对于函数的 interface, 咱们可否进行一些特殊处理, 好比, 对一个已有的函数定义, 提取其第 2 个参数的参数类型? 对于下面这个 func, 咱们可否提取出 arg2 的类型?


interface func () {
    (arg1: string, arg2: {
        bar: string
    }): void
}复制代码


下一篇咱们会讨论, 如何使用 infer 关键字达成这一目标.


其它


基于泛型的类型推导, 理论上从 typescript 2.8 开始(实际上更早, 但 typescript 2.8/3.5 是具备里程碑意义的版本, 故以此划分)就能够实现了, 从 typescript 3.5, 官方内置了一些用于推导的的类型(#type)和接口(#interface), 这些是咱们用于学习类型推导的良好案例. 本文用到的全部例子, 都来自于 typescript 内置的 lib.es5.d.ts.


注意 对于泛型, T 每每是用做泛型标记的第一个选择, T 之于泛型, 比如 foo 之于样例代码

相关文章
相关标签/搜索