专栏首页程序员成长指北TypeScript 强大的类型别名

TypeScript 强大的类型别名

作者:MervynZ

链接:https://juejin.im/post/5c2f87ce5188252593122c98

TS 有个非常好用的功能就是类型别名。

类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

一些关键字

使用类型别名可以实现很多复杂的类型,很多复杂的类型别名都需要借助关键字,我们先来了解一下几个常用的关键字:

extends

extends 可以用来继承一个类,也可以用来继承一个 interface,但还可以用来判断有条件类型:

T extends U ? X : Y;

上面的类型意思是,若T 能够赋值给U,那么类型是 X,否则为 Y。 原理是令T'U'分别为TU 的实例,并将所有类型参数替换为any,如果T'能赋值给 U',则将有条件的类型解析成 X,否则为Y。 上面的官方解释有点绕,下面举个栗子:

type Words = 'a'|'b'|"c";

type W<T> = T extends Words ? true : false;

type WA = W<'a'>; // -> true
type WD = W<'d'>; // -> false

a可以赋值给 Words 类型,所以 WA 为 true,而 d 不能赋值给 Words 类型,所以 WDfalse

typeof

在 JS 中 typeof 可以判断一个变量的基础数据类型,在 TS 中,它还有一个作用,就是获取一个变量的声明类型,如果不存在,则获取该类型的推论类型。 举两个栗子:

interface Person {
  name: string;
  age: number;
  location?: string;
}

const jack: Person = { name: 'jack', age: 100 };
type Jack = typeof jack; // -> Person

function foo(x: number): Array<number> {
  return [x];
}

type F = typeof foo; // -> (x: number) => number[]

Jack 这个类型别名实际上就是 jack 的类型 Person,而 F 的类型就是 TS 自己推导出来的 foo 的类型 (x: number) => number[]

keyof

keyof 可以用来取得一个对象接口的所有 key 值:

interface Person {
    name: string;
    age: number;
    location?: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string | number

in

in 可以遍历枚举类型:

type Keys = "a" | "b"
type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any }

上面 in 遍历 Keys,并为每个值赋予any类型。

infer

在条件类型语句中, 可以用 infer 声明一个类型变量并且对它进行使用, 我们可以用它获取函数的返回类型, 源码如下:

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any;

其实这里的 infer R就是声明一个变量来承载传入函数签名的返回值类型, 简单说就是用它取到函数返回值的类型方便之后使用。

内置类型别名

下面我们看一下 TS 内置的一些类型别名:

Partial

Partial 的作用就是可以将某个类型里的属性全部变为可选项 ?。源码:

// node_modules/typescript/lib/lib.es5.d.ts

type Partial<T> = {
    [P in keyof T]?: T[P];
};

从源码可以看到 keyof T 拿到 T 所有属性名, 然后 in进行遍历, 将值赋给 P, 最后 T[P]取得相应属性的值. 结合中间的 ?,将所有属性变为可选.

Required

Required 的作用刚好跟 Partial 相反,Partial 是将所有属性改成可选项,Required 则是将所有类型改成必选项,源码如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Required<T> = {
    [P in keyof T]-?: T[P];
};

其中 -? 是代表移除 ? 这个 modifier的标识。 与之对应的还有个 +?, 这个含义自然与 -? 之前相反, 它是用来把属性变成可选项的,+ 可省略,见 Partial。 再拓展一下,除了可以应用于 ? 这个 modifiers ,还有应用在 readonly ,比如 Readonly.

Readonly

这个类型的作用是将传入的属性变为只读选项。

// node_modules/typescript/lib/lib.es5.d.ts

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

给子属性添加 readonly 的标识,如果将上面的 readonly 改成 -readonly, 就是移除子属性的 readonly 标识。

Pick

这个类型则可以将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。 源码实现如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

从源码可以看到 K 必须是 Tkey,然后用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。

Record

该类型可以将 K 中所有的属性的值转化为 T 类型,源码实现如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

可以根据 K 中的所有可能值来设置 key,以及 value 的类型,举个例子:

type T11 = Record<'a' | 'b' | 'c', Person>; // -> { a: Person; b: Person; c: Person; }

Exclude

Exclude 将某个类型中属于另一个的类型移除掉。 源码的实现:

// node_modules/typescript/lib/lib.es5.d.ts

type Exclude<T, U> = T extends U ? never : T;

以上语句的意思就是 如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T,最终结果是将 T 中的某些属于 U 的类型移除掉,举个例子:

type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'b' | 'd'

可以看到 T'a' | 'b' | 'c' | 'd',然后 U'a' | 'c' | 'f' ,返回的新类型就可以将U中的类型给移除掉,也就是 'b' | 'd' 了。

Extract

Extract 的作用是提取出 T 包含在 U 中的元素,换种更加贴近语义的说法就是从 T 中提取出 U,源码如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Extract<T, U> = T extends U ? T : never;

以上语句的意思就是 如果 T 能赋值给 U 类型的话,那么就会返回 T 类型,否则返回 never,最终结果是将 TU 中共有的属性提取出来,举个例子:

type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'a' | 'c'

可以看到 T'a' | 'b' | 'c' | 'd' ,然后 U'a' | 'c' | 'f' ,返回的新类型就可以将 TU 中共有的属性提取出来,也就是 'a' | 'c'了。

ReturnType

该类型的作用是获取函数的返回类型。 源码的实现

// node_modules/typescript/lib/lib.es5.d.ts

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

实际使用的话,就可以通过 ReturnType 拿到函数的返回类型,如下的示例:

function foo(x: number): Array<number> {
  return [x];
}

type fn = ReturnType<typeof foo>; // -> number[]

ThisType

这个类型是用于指定上下文对象类型的。

// node_modules/typescript/lib/lib.es5.d.ts

interface ThisType<T> { }

可以看到声明中只有一个接口,没有任何的实现,说明这个类型是在 TS 源码层面支持的,而不是通过类型变换。 这类型怎么用呢,举个例子:

interface Person {
    name: string;
    age: number;
}

const obj: ThisType<Person> = {
  dosth() {
    this.name // string
  }
}

这样的话,就可以指定obj里的所有方法里的上下文对象改成Person 这个类型了。

InstanceType

该类型的作用是获取构造函数类型的实例类型。 源码实现:

// node_modules/typescript/lib/lib.es5.d.ts

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

看一下官方的例子:

class C {
    x = 0;
    y = 0;
}

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

NonNullable

这个类型可以用来过滤类型中的 nullundefined类型。 源码实现:

// node_modules/typescript/lib/lib.es5.d.ts

type NonNullable<T> = T extends null | undefined ? never : T;

比如:

type T22 = string | number | null;
type T23 = NonNullable<T22>; // -> string | number;

Parameters

该类型可以获得函数的参数类型组成的元组类型。 源码实现:

// node_modules/typescript/lib/lib.es5.d.ts

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;

举个栗子:

function foo(x: number): Array<number> {
  return [x];
}

type P = Parameters<typeof foo>; // -> [number]

此时 P 的真实类型就是 foo 的参数组成的元组类型 [number]

ConstructorParameters

该类型的作用是获得类的参数类型组成的元组类型,源码:

// node_modules/typescript/lib/lib.es5.d.ts

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;

举个栗子:

class Person {
  private firstName: string;
  private lastName: string;

  constructor(firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;
  }
}

type P = ConstructorParameters<typeof Person>; // -> [string, string]

此时 P 就是 Personconstructor 的参数 firstNamelastName 的类型所组成的元组类型 [string, string]

自定义类型别名

下面是一些可能会经常用到,但是 TS 没有内置的一些类型别名:

Omit

有时候我们想要继承某个接口,但是又需要在新接口中将某个属性给 overwrite 掉,这时候通过 PickExclude 就可以组合出来 Omit,用来忽略对象某些属性功能:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }

Mutable

T 的所有属性的 readonly 移除:

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}

PowerPartial

内置的 Partial 有个局限性,就是只支持处理第一层的属性,如果是嵌套多层的就没有效果了,不过可以如下自定义:

type PowerPartial<T> = {
    // 如果是 object,则递归类型
    [U in keyof T]?: T[U] extends object
      ? PowerPartial<T[U]>
      : T[U]
};

Deferred

相同的属性名称,但使值是一个 Promise,而不是一个具体的值:

type Deferred<T> = {
    [P in keyof T]: Promise<T[P]>;
};

Proxify

T 的属性添加代理

type Proxify<T> = {
    [P in keyof T]: { get(): T[P]; set(v: T[P]): void }
};

如有疑问,欢迎斧正!

参考

  • TypeScript 中文网
  • TS 中的内置类型简述
  • TypeScript 一些你可能不知道的工具泛型的使用及其实现

本文分享自微信公众号 - 程序员成长指北(coder_growth),作者:MervynZ

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一道面试题引发的事件循环深入思考

    面试题如下,大家可以先试着写一下输出结果,看与正确答案是否有出入,如果大家不能准确的做出答案,可以通过下面对微任务,事件循环,定时器等相关代码执行顺序的讲解,让...

    coder_koala
  • 从 JavaScript 发展历史中聊 ECMAScript(ES6-ES11) 新功能

    JavaScript 是当今使用最广泛的、发展最好的前后端(后端主要是 Nodejs)语言,如果我们想要灵活使用 JavaScript,我们首先需要了解的就是 ...

    coder_koala
  • 如何答一道惊艳面试官的数组去重问题?

    思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个...

    coder_koala
  • 遇到这些 TS 问题你会头晕么?

    相信很多读者看到 let value: Fonum = 12; 这一行,TS 编译器并未提示任何错误会感到惊讶。很明显数字 12 并不是 Fonum 枚举的成员...

    阿宝哥
  • 小程序 · 一周报

    据 IT 之家消息 QQ 内测版 7.8.0 for Android 上线了 QQ 小程序集合「轻应用」,用户可通过聊天列表界面右上角的「+」号,看到 QQ 小...

    极乐君
  • 开发一个智能客服需要多少钱?

    现在很多网站的客服人员都会采用智能的聊天机器人回复客户的咨询问题,那如果要开发一个这样的聊天机器人,需要花费多少钱?

    人工智能的秘密
  • 让早期肺癌不再难发现 “腾讯觅影” 在延安这家医院启动应用

    12月14日,腾讯人工智能(AI)医疗影像"腾讯觅影"在延安大学附属医院全面启动应用,通过"腾讯觅影"辅助医生开展智能辅助筛查,将有效提高医生的诊断效率和精度,...

    企鹅号小编
  • Java电商系统商品详情页存储方案设计

    不管什么电商系统,商品详情页一定是整个系统中日均访问次数最高的页面之一.不难理解,用户购物,看商品详情不一定买,一定会看好多商品详情页货比三家.如果在设计存储时...

    JavaEdge
  • 实测!微信刚推出的「小店」小程序,我们用 10 分钟就完成了申请(附攻略)

    只要你有一个认证的公众号,就可在公众号后台申请小店小程序。原有微信小店功能的公众号,可直接升级,不需要再重复申请。

    知晓君
  • 科学减肥就靠它!这款小程序,让你边吃边瘦

    减肥过程中最重要并且最痛苦的,就是不能吃高热量食物,比如巧克力、蛋糕、薯片等。但是,除了这些显而易见的之外,还有哪些隐藏起来的高热量「减肥杀手」呢?

    知晓君

扫码关注云+社区

领取腾讯云代金券