一文带你了解TypeScript高级类型

秒速五厘米 2022-12-31 11:29 406阅读 0赞

目录

      • 本文概览:
        1. 交叉类型
        1. 联合类型
        1. 字面量类型
        • (1)字符串字面量类型
        • (2)数字字面量类型
        1. 索引类型
        • (1)索引类型查询操作符
        • (2)索引访问操作符
        1. 映射类型
        • (1)由映射类型进行推断
        • (2)增加或移除特定修饰符
        • (3)keyof 和映射类型在 2.9版本的升级
        • (4)元组和数组上的映射类型
        1. 条件类型
        • (1)条件类型基础使用
        • (2)分布式条件类型
        • (3)条件类型的类型推断-infer
        • (4)预定义条件类型
        • (5)条件类型与映射类型
        1. 可辨识联合类型
        • (1)利用 strictNullChecks
        • (2) 使用 never 类型
        1. 类型别名

本文概览:

在这里插入图片描述

1. 交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

在 JavaScript 中,混入是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象会拥有着两个对象所有的功能。

交叉类型可以让我们安全的使用此种模式:

  1. function mixin<T, U>(first: T, second: U): T & U {
  2. const result = <T & U>{ };
  3. for (let id in first) {
  4. (<T>result)[id] = first[id];
  5. }
  6. for (let id in second) {
  7. if (!result.hasOwnProperty(id)) {
  8. (<U>result)[id] = second[id];
  9. }
  10. }
  11. return result;
  12. }
  13. const x = extend({ a: 'hello' }, { b: 42 });
  14. // 现在 x 拥有了 a 属性与 b 属性
  15. const a = x.a;
  16. const b = x.b;

2. 联合类型

在 JavaScript 中,我们希望属性为多种类型之一,如字符串或者数组。
这就是联合类型所能派上用场的地方(它使用 | 作为标记,如 string | number)。

  1. function formatCommandline(command: string[] | string) {
  2. let line = '';
  3. if (typeof command === 'string') {
  4. line = command.trim();
  5. } else {
  6. line = command.join(' ').trim();
  7. }
  8. }

联合类型表示一个值可以是几种类型之一,用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是number、string、或boolean。

3. 字面量类型

字面量类型可能也算不上是高级类型,但是字符串字面量类型和字符串类型其实并不一样,下面来看一下它们有什么区别。

(1)字符串字面量类型

字符串字面量类型其实就是字符串常量,与字符串类型不同的是它是具体的值。

  1. type Name = "TS";
  2. const name1: Name = "test"; // error 不能将类型"test"分配给类型"TS"
  3. const name2: Name = "TS";

还可以使用联合类型来使用多个字符串:

  1. type Direction = "north" | "east" | "south" | "west";
  2. function getDirectionFirstLetter(direction: Direction) {
  3. return direction.substr(0, 1);
  4. }
  5. getDirectionFirstLetter("test"); // error 类型“"test"”的参数不能赋给类型“Direction”的参数
  6. getDirectionFirstLetter("east");

(2)数字字面量类型

除了字符串字面量类型还有数字字面量类型,它和字符串字面量类型差不多,都是指定类型为具体的值。

  1. type Age = 18;
  2. interface Info {
  3. name: string;
  4. age: Age;
  5. }
  6. const info: Info = {
  7. name: "TS",
  8. age: 28 // error 不能将类型“28”分配给类型“18”
  9. };

来看一个比较经典的逻辑错误:

  1. function getValue(index: number) {
  2. if (index !== 0 || index !== 1) {
  3. // error This condition will always return 'true' since the types '0' and '1' have no overlap
  4. // ...
  5. }
  6. }

在判断逻辑处使用了 || 符,当 index !== 0 不成立时,说明 index 就是 0,则不应该再判断 index 是否不等于 1;而如果 index !== 0 成立,那后面的判断也不会再执行;所以这个地方会报错。

4. 索引类型

我们先看一个场景,现在需要一个 pick 函数,这个函数可以从对象上取出指定的属性,在 JavaScript 中这个函数应该是这样的:

  1. function pick(o, names) {
  2. return names.map(n => o[n]);
  3. }

如果从一个 user 对象中取出 id ,那么应该这样:

  1. const user = {
  2. username: 'Jessica Lee',
  3. id: 460000201904141743,
  4. token: '460000201904141743',
  5. avatar: 'http://dummyimage.com/200x200',
  6. role: 'vip'
  7. }
  8. const res = pick(user, ['id'])
  9. console.log(res) // [ '460000201904141743' ]

那如何在 TypeScript 中实现上述函数?
pick 函数的第一个参数 o 可以使用可索引类型,这个对象的 key 都是 string 而对应的值可能是任意类型,那么可以这样表示:

  1. interface Obj {
  2. [key: string]: any
  3. }

第二个参数 names 很明显是个字符串数组:

  1. function pick(o: Obj, names: string[]) {
  2. return names.map(n => o[n]);
  3. }

这样写定义不够严谨:

  • 参数 names 的成员应该是参数 o 的属性,因此不应该是 string 这种宽泛的定义,应该更加准确
  • pick 函数的返回值类型为 any[],其实可以更加精准,pick 的返回值类型应该是所取的属性值类型的联合类型

要想定义更精准的定义类型必须先了解两个类型操作符:索引类型查询操作符索引访问操作符

(1)索引类型查询操作符

keyof操作符,连接一个类型,会返回一个由这个类型的所有属性名组成的联合类型:

  1. interface Info {
  2. name: string;
  3. age: number;
  4. }
  5. let infoProp: keyof Info;
  6. infoProp = "name";
  7. infoProp = "age";
  8. infoProp = "no"; // error 不能将类型“"no"”分配给类型“"name" | "age"”

这里的keyof Info其实相当于"name" | “age”。通过和泛型结合使用,TS 就可以检查使用了动态属性名的代码:

  1. function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] { // 这里使用泛型,并且约束泛型变量K的类型是"keyof T",也就是类型T的所有字段名组成的联合类型
  2. return names.map(n => obj[n]); // 指定getValue的返回值类型为T[K][],即类型为T的值的属性值组成的数组
  3. }
  4. const info = {
  5. name: "lison",
  6. age: 18
  7. };
  8. let values: string[] = getValue(info, ["name"]);
  9. values = getValue(info, ["age"]); // error 不能将类型“number[]”分配给类型“string[]”

keyof 正是赋予了开发者查询索引类型的能力。

(2)索引访问操作符

索引访问操作符也就是[],其实和我们访问对象的某个属性值是一样的语法,但是在 TS 中它可以用来访问某个属性的类型:

  1. interface Info {
  2. name: string;
  3. age: number;
  4. }
  5. type NameType = Info["name"];
  6. let name: NameType = 123; // error 不能将类型“123”分配给类型“string”

再来看个例子:

  1. function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
  2. return o[name]; // o[name] is of type T[K]
  3. }

这里两个参数的类型分别为泛型 T 和 K,而函数的返回值类型为T[K],只要函数的返回值也是这种形式,即访问参数 o 的参数 name 属性,即可。

再来看个结合接口的例子:

  1. interface Obj<T> {
  2. [key: number]: T;
  3. }
  4. const key: keyof Obj<number>; // keys的类型为number

注意,如果索引类型为 number,那么实现该接口的对象的属性名必须是 number 类型;但是如果接口的索引类型是 string 类型,那么实现该接口的对象的属性名设置为数值类型的值也是可以的,因为数值最后还是会先转换为字符串。这里一样,如果接口的索引类型设置为 string 的话,keyof Obj<number>等同于类型number | string

  1. interface Obj<T> {
  2. [key: string]: T;
  3. }
  4. let key: keyof Obj<number>; // keys的类型为number | string
  5. key = 123; // right

也可以使用访问操作符,获取索引签名的类型:

  1. interface Obj<T> {
  2. [key: string]: T;
  3. }
  4. const obj: Obj<number> = {
  5. age: 18
  6. };
  7. let value: Obj<number>["age"]; // value的类型是number,也就是name的属性值18的类型

还有一点,当tsconfig.json里strictNullChecks设为false时,通过Type[keyof Type]获取到的,是除去never & undefined & null这三个类型之后的字段值类型组成的联合类型:

  1. interface Type {
  2. a: never;
  3. b: never;
  4. c: string;
  5. d: number;
  6. e: undefined;
  7. f: null;
  8. g: object;
  9. }
  10. type test = Type[keyof Type];
  11. // test的类型是string | number | object

这里接口 Type 有几个属性,通过索引访问操作符和索引类型查询操作符可以选出类型不为 never & undefined & null 的类型。

当了解了这两个访问符之后,最开始的问题就迎刃而解了。

首先,需要一个泛型 T 它来代表传入的参数 o 的类型,因为在编写代码时无法确定参数 o 的类型到底是什么,所以在这种情况下要获取 o 的类型必须用面向未来的类型–泛型。

那么传入的第二个参数 names ,它的特点就是数组的成员必须由参数 o 的属性名称构成,这个时候我们很容易想到操作符keyof, keyof T代表参数o类型的属性名的联合类型,参数names的成员类型K则只需要约束到keyof T即可。

返回值就更简单了,通过类型访问符T[K]便可以取得对应属性值的类型,他们的数组T[K][]正是返回值的类型。

  1. function pick<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  2. return names.map(n => o[n]);
  3. }
  4. const res = pick(user, ['token', 'id', ])

用索引类型结合类型操作符完成了 TypeScript 版的 pick 函数,它不仅仅有更严谨的类型约束能力,也提供了更强大的代码提示能力:
在这里插入图片描述

5. 映射类型

现在有一需求,有一个User接口,现在有一个需求是把User接口中的成员全部变成可选的,我们应该怎么做?难道要重新一个个:前面加上?,有没有更便捷的方法?

  1. interface User {
  2. username: string
  3. id: number
  4. token: string
  5. avatar: string
  6. role: string
  7. }

这个时候映射类型就派上用场了,映射类型的语法是[K in Keys]:

  • K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
  • Keys:字符串字面量构成的联合类型,表示一组属性名(的类型)

首先需要找到Keys,即字符串字面量构成的联合类型,这就得使用上面提到的keyof操作符,假设传入的类型是泛型T,得到keyof T,即传入类型T的属性名的联合类型。

然后我们需要将keyof T的属性名称一一映射出来[K in keyof T],如果要把所有的属性成员变为可选类型,那么需要T[K]取出相应的属性值,最后重新生成一个可选的新类型{ [K in keyof T]?: T[K] }

用类型别名表示就是:

  1. type partial<T> = { [K in keyof T]?: T[K] }

测试一下:

  1. type partialUser = partial<User>

所有的属性都变成了可选类型:
在这里插入图片描述

(1)由映射类型进行推断

使用映射类型包装一个类型的属性后,也可以进行逆向操作,也就是拆包,先来看包装操作:

  1. type Proxy<T> = { // 这里定义一个映射类型,他将一个属性拆分成get/set方法
  2. get(): T;
  3. set(value: T): void;
  4. };
  5. type Proxify<T> = { [P in keyof T]: Proxy<T[P]> }; // 这里再定义一个映射类型,将一个对象的所有属性值类型都变为Proxy<T>处理之后的类型
  6. function proxify<T>(obj: T): Proxify<T> { // 这里定义一个proxify函数,用来将对象中所有属性的属性值改为一个包含get和set方法的对象
  7. let result = { } as Proxify<T>;
  8. for (const key in obj) {
  9. result[key] = {
  10. get: () => obj[key],
  11. set: value => (obj[key] = value)
  12. };
  13. }
  14. return result;
  15. }
  16. let props = {
  17. name: "lison",
  18. age: 18
  19. };
  20. let proxyProps = proxify(props);
  21. console.log(proxyProps.name.get()); // "lison"
  22. proxyProps.name.set("li");

这里我们定义了一个函数,这个函数可以把传入的对象的每个属性的值替换为一个包含 get 和 set 两个方法的对象。最后我们获取某个值的时候,比如 name,就使用 proxyProps.name.get()方法获取它的值,使用 proxyProps.name.set()方法修改 name 的值。

接下来进行拆包:

  1. function unproxify<T>(t: Proxify<T>): T { // 这里我们定义一个拆包函数,其实就是利用每个属性的get方法获取到当前属性值,然后将原本是包含get和set方法的对象改为这个属性值
  2. let result = { } as T;
  3. for (const k in t) {
  4. result[k] = t[k].get(); // 这里通过调用属性值这个对象的get方法获取到属性值,然后赋给这个属性,替换掉这个对象
  5. }
  6. return result;
  7. }
  8. let originalProps = unproxify(proxyProps);

(2)增加或移除特定修饰符

TS 在 2.8 版本为映射类型增加了增加或移除特定修饰符的能力,使用+-符号作为前缀来指定增加还是删除修饰符。首先来看如何通过映射类型为一个接口的每个属性增加修饰符,这里使用+前缀:

  1. interface Info {
  2. name: string;
  3. age: number;
  4. }
  5. type ReadonlyInfo<T> = { +readonly [P in keyof T]+?: T[P] };
  6. let info: ReadonlyInfo<Info> = {
  7. name: "lison"
  8. };
  9. info.name = ""; // error

经过 ReadonlyInfo 创建的接口类型,属性是可选的,所以在定义 info 的时候没有写 age 属性也没问题,同时每个属性是只读的,所以修改 name 的值的时候报错。通过+前缀增加了 readonly 和?修饰符。当然,增加的时候,这个+前缀可以省略,也就是说,上面的写法和type ReadonlyInfo = { readonly [P in keyof T]?: T[P] }是一样的。

删除修饰符:

  1. interface Info {
  2. name: string;
  3. age: number;
  4. }
  5. type RemoveModifier<T> = { -readonly [P in keyof T]-?: T[p] };
  6. type InfoType = RemoveModifier<Readonly<Partial<Info>>>;
  7. let info1: InfoType = {
  8. // error missing "age"
  9. name: "lison"
  10. };
  11. let info2: InfoType = {
  12. name: "lison",
  13. age: 18
  14. };
  15. info2.name = ""; // right, can edit

这里定义了去掉修饰符的映射类型 RemoveModifier,Readonly<Partial<Info>>则是返回一个既属性可选又只读的接口类型,所以 InfoType 类型则表示属性必含而且非只读。

TS 内置了一个映射类型Required<T>,使用它可以去掉 T 所有属性的?修饰符。

(3)keyof 和映射类型在 2.9版本的升级

TS 在 2.9 版本中,keyof 和映射类型支持用 number 和 symbol 命名的属性,下面是 keyof 的例子:

  1. const stringIndex = "a";
  2. const numberIndex = 1;
  3. const symbolIndex = Symbol();
  4. type Obj = {
  5. [stringIndex]: string;
  6. [numberIndex]: number;
  7. [symbolIndex]: symbol;
  8. };
  9. type keys = keyof Obj;
  10. let key: keys = 2; // error
  11. let key: keys = 1; // right
  12. let key: keys = "b"; // error
  13. let key: keys = "a"; // right
  14. let key: keys = Symbol(); // error
  15. let key: keys = symbolIndex; // right

再来看映射类型的例子:

  1. const stringIndex = "a";
  2. const numberIndex = 1;
  3. const symbolIndex = Symbol();
  4. type Obj = {
  5. [stringIndex]: string;
  6. [numberIndex]: number;
  7. [symbolIndex]: symbol;
  8. };
  9. type ReadonlyType<T> = { readonly [P in keyof T]?: T[P] };
  10. let obj: ReadonlyType<Obj> = {
  11. a: "aa",
  12. 1: 11,
  13. [symbolIndex]: Symbol()
  14. };
  15. obj.a = "bb"; // error Cannot assign to 'a' because it is a read-only property
  16. obj[1] = 22; // error Cannot assign to '1' because it is a read-only property
  17. obj[symbolIndex] = Symbol(); // error Cannot assign to '[symbolIndex]' because it is a read-only property

(4)元组和数组上的映射类型

TS 在 3.1 版本中,在元组和数组上的映射类型会生成新的元组和数组,并不会创建一个新的类型,这个类型上会具有 push、pop 等数组方法和数组属性:

  1. type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };
  2. type Tuple = [number, string, boolean];
  3. type promiseTuple = MapToPromise<Tuple>;
  4. let tuple: promiseTuple = [
  5. new Promise((resolve, reject) => resolve(1)),
  6. new Promise((resolve, reject) => resolve("a")),
  7. new Promise((resolve, reject) => resolve(false))
  8. ];

这里定义了一个MapToPromise映射类型。它返回一个将传入的类型的所有字段的值转为Promise,且Promise的resolve回调函数的参数类型为这个字段类型。定义一个元组Tuple,元素类型分别为number、string和boolean,使用MapToPromise映射类型将这个元组类型传入,并且返回一个promiseTuple类型。当指定变量tuple的类型为promiseTuple后,它的三个元素类型都是一个Promise,且resolve的参数类型依次为number、string和boolean。

6. 条件类型

(1)条件类型基础使用

条件类型是 TS 在2.8版本引入的,从语法上看它像是三元操作符。它会以一个条件表达式进行类型关系检测,然后在后面两种类型中选择一个,写法如下:

  1. T extends U ? X : Y

这个表达式的意思是,如果 T 可以赋值给 U 类型,则是 X 类型,否则是 Y 类型。

比如,声明一个函数 f,它的参数接收一个布尔类型,当布尔类型为 true 时返回 string 类型,否则返回 number 类型:

  1. declare function f<T extends boolean>(x: T): T extends true ? string : number;
  2. const x = f(Math.random() < 0.5) // x类型: const x: string | number
  3. const y = f(false) // y类型: const y: number
  4. const z = f(true) // z类型: const z: string

条件类型就是这样,只有类型系统中给出充足的条件之后,它才会根据条件推断出类型结果。

(2)分布式条件类型

当待检测的类型是联合类型,则该条件类型被称为“分布式条件类型”,在实例化时会自动分发成联合类型:

  1. type TypeName<T> = T extends any ? T : never;
  2. type Type1 = TypeName<string | number>; // Type1的类型是string|number

再来看个复杂点的例子,这是官方文档的例子:

  1. type TypeName<T> = T extends string
  2. ? string
  3. : T extends number
  4. ? number
  5. : T extends boolean
  6. ? boolean
  7. : T extends undefined
  8. ? undefined
  9. : T extends Function
  10. ? Function
  11. : object;
  12. type Type1 = TypeName<() => void>; // Type1的类型是Function
  13. type Type2 = TypeName<string[]>; // Type2的类型是object
  14. type Type3 = TypeName<(() => void) | string[]>; // Type3的类型是object | Function

来看一个分布式条件类型的实际应用:

  1. type Diff<T, U> = T extends U ? never : T;
  2. type Test = Diff<string | number | boolean, undefined | number>;
  3. // Test的类型为string | boolean

这里定义的条件类型的作用就是,找出从 T 中出去 U 中存在的类型,得到剩下的类型。不过这个条件类型已经内置在 TS 中了,只不过它不叫 Diff,叫 Exclude。

来看一个条件类型和映射类型结合的例子:

  1. type Type<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
  2. interface Part {
  3. id: number;
  4. name: string;
  5. subparts: Part[];
  6. updatePart(newName: string): void;
  7. }
  8. type Test = Type<Part>; // Test的类型为"updatePart"

这个例子中,接口 Part 有四个字段,其中 updatePart 的值是函数,也就是 Function 类型。Type的定义中,涉及到映射类型、条件类型、索引访问类型和索引类型。首先[K in keyof T]用于遍历 T 的所有属性名,值使用了条件类型,T[K]是当前属性名的属性值,T[K] extends Function ? K : never表示如果属性值为 Function 类型,则值为属性名字面量类型,否则为 never 类型。接下来使用keyof T获取 T 的属性名,最后通过索引访问类型[keyof T]获取不为 never 的类型。

(3)条件类型的类型推断-infer

条件类型提供一个infer关键字用来推断类型。我们想定义一个条件类型,如果传入的类型是一个数组,则返回它元素的类型;如果是一个普通类型,则直接返回这个类型。不使用 infer可以这样写:

  1. type Type<T> = T extends any[] ? T[number] : T;
  2. type test = Type<string[]>; // test的类型为string
  3. type test2 = Type<string>; // test2的类型为string

如果传入 Type 的是一个数组类型,那么返回的类型为T[number],也就是该数组的元素类型,如果不是数组,则直接返回这个类型。这里通过索引访问类型T[number]来获取类型的,如果使用 infer 关键字则无需自己手动获取,来看下怎么使用 infer:

  1. type Type<T> = T extends Array<infer U> ? U : T;
  2. type test = Type<string[]>; // test的类型为string
  3. type test2 = Type<string>; // test2的类型为string

这里 infer 能够推断出 U 的类型,并且供后面使用,可以理解为这里定义了一个变量 U 来接收数组元素的类型。

(4)预定义条件类型

TS 在 2.8 版本增加了一些预定义的有条件类型,来看一下:

  • Exclude,从 T 中去掉可以赋值给 U 的类型:

    type Type = Exclude<”a” | “b” | “c”, “a” | “b”>;
    // Type => ‘c’
    type Type2 = Exclude;
    // Type2 => boolean

  • Extract,选取 T 中可以赋值给 U 的类型:

    type Type = Extract<”a” | “b” | “c”, “a” | “c” | “f”>;
    // Type => ‘a’ | ‘c’
    type Type2 = Extract;
    // Type2 => string | boolean

  • NonNullable,从 T 中去掉 null 和 undefined:

    type Type = Extract;
    // Type => string | number

  • ReturnType,获取函数类型返回值类型:

    type Type = ReturnType<() => string)>
    // Type => string
    type Type2 = ReturnType<(arg: number) => void)>
    // Type2 => void

  • InstanceType,获取构造函数类型的实例类型:

先来看下InstanceType的实现:

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

InstanceType 条件类型要求泛型变量 T 类型是创建实例为 any 类型的构造函数,而它本身则通过判断 T 是否是构造函数类型来确定返回的类型。如果是构造函数,使用 infer 可以自动推断出 R 的类型,即实例类型;否则返回的是 any 类型。

再来看下InstanceType 的使用:

  1. class A {
  2. constructor() { }
  3. }
  4. type T1 = InstanceType<typeof A>; // T1的类型为A
  5. type T2 = InstanceType<any>; // T2的类型为any
  6. type T3 = InstanceType<never>; // T3的类型为never
  7. type T4 = InstanceType<string>; // error

在T1 的定义中,typeof A返回的的是类 A 的类型,也就是 A,这里不能使用 A 因为它是值不是类型,类型 A 是构造函数,所以 T1 是 A 构造函数的实例类型,也就是 A;T2 传入的类型为 any,因为 any 是任何类型的子类型,所以它满足T extends new (…args: any[]) => infer R,这里 infer 推断的 R 为 any;传入 never 和 any 同理。传入 string 时因为 string 不能不给构造函数类型,所以报错。

(5)条件类型与映射类型

思考题: 有一个interface Part。现在需要编写一个工具类型将interface中函数类型名称取出来,在这个题目示例中,应该取出的是:
在这里插入图片描述

  1. interface Part {
  2. id: number;
  3. name: string;
  4. subparts: Part[];
  5. updatePart(newName: string): void;
  6. }
  7. type R = FunctionPropertyNames<Part>;

那该如何设计这个工具类型?这种问题我们应该换个思路,比如把interface看成js中的对象字面量,用js的思维只要遍历整个对象,找出value是函数的部分取出key即可.

在TypeScript的类型编程中也是类似的道理,要遍历interface,取出类型为Function的部分找出key即可:

  1. type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]

下面来一步步分析一下上述工具类型:

  1. 假设把Part代入泛型T[K in keyof T]相当于遍历整个interface
  2. 这时K相当于interface的key,T[K]相当于interface的value
  3. 接下来,用条件类型验证value的类型,如果是Function那么将value作为新interface的key保留下来,否则为never
  4. 到这里我们得到了遍历修改后的interface:

    type R = {

    1. id: never;
    2. name: never;
    3. subparts: never;
    4. updatePart: "updatePart";

    }

特别注意: 这里产生的新interface R中的value是老interface Part的key,取出新interface R的value就是取出了对应老interface Part的key。

但是要求的是取出老interface Part的key,这个时候再次用[keyof T]作为key依次取出新interface的value,但是由于id namesubparts的value为never就不会返回任何类型了,所以只返回了'updatePart'.

7. 可辨识联合类型

我们可以把单例类型、联合类型、类型保护和类型别名这几种类型进行合并,来创建一个叫做可辨识联合的高级类型,它也可称作标签联合代数数据类型

所谓单例类型,可以理解为符合单例模式的数据类型,比如枚举成员类型,字面量类型。

可辨识联合要求具有两个要素:

  • 具有普通的单例类型属性(这个要作为辨识的特征,也是重要因素)。
  • 一个类型别名,包含了那些类型的联合(即把几个类型封装为联合类型,并起一个别名)。

可辨识联合类型就是为了保证每个case都能被处理。

来看一个例子:

  1. interface Square {
  2. kind: "square"; // 这个就是具有辨识性的属性
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: "rectangle"; // 这个就是具有辨识性的属性
  7. height: number;
  8. width: number;
  9. }
  10. interface Circle {
  11. kind: "circle"; // 这个就是具有辨识性的属性
  12. radius: number;
  13. }
  14. type Shape = Square | Rectangle | Circle; // 这里使用三个接口组成一个联合类型,并赋给一个别名Shape,组成了一个可辨识联合。
  15. function getArea(s: Shape) {
  16. switch (s.kind) {
  17. case "square":
  18. return s.size * s.size;
  19. case "rectangle":
  20. return s.height * s.width;
  21. case "circle":
  22. return Math.PI * s.radius ** 2;
  23. }
  24. }

上面这个例子中,我们的 Shape 即可辨识联合,它是三个接口的联合,而这三个接口都有一个 kind 属性,且每个接口的 kind 属性值都不相同,能够起到标识作用。 函数内应该包含联合类型中每一个接口的 case。

如果函数内没有包含联合类型中每一个接口的 case。但是如果遗漏了,就希望编译器应该给出提示。有以下两种完整性检查的方法:利用 strictNullChecks使用 never 类型

(1)利用 strictNullChecks

对上面的例子加一种接口:

  1. interface Square {
  2. kind: "square";
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: "rectangle";
  7. height: number;
  8. width: number;
  9. }
  10. interface Circle {
  11. kind: "circle";
  12. radius: number;
  13. }
  14. interface Triangle {
  15. kind: "triangle";
  16. bottom: number;
  17. height: number;
  18. }
  19. type Shape = Square | Rectangle | Circle | Triangle; // 这里我们在联合类型中新增了一个接口,但是下面的case却没有处理Triangle的情况
  20. function getArea(s: Shape) {
  21. switch (s.kind) {
  22. case "square":
  23. return s.size * s.size;
  24. case "rectangle":
  25. return s.height * s.width;
  26. case "circle":
  27. return Math.PI * s.radius ** 2;
  28. }
  29. }

这里,Shape 联合有四种接口,但函数的 switch 里只包含三个 case,这个时候编译器并没有提示任何错误,因为当传入函数的是类型是 Triangle 时,没有任何一个 case 符合,则不会有 return 语句执行,那么函数是默认返回 undefined。所以我们可以利用这个特点,结合 strictNullChecks编译选项,可以开启 strictNullChecks,然后让函数的返回值类型为 number,那么当返回 undefined 的时候,就会报错:

  1. function getArea(s: Shape): number {
  2. // error Function lacks ending return statement and return type does not include 'undefined'
  3. switch (s.kind) {
  4. case "square":
  5. return s.size * s.size;
  6. case "rectangle":
  7. return s.height * s.width;
  8. case "circle":
  9. return Math.PI * s.radius ** 2;
  10. }
  11. }

这种方法简单,但是对旧代码支持不好,因为strictNullChecks这个配置项是2.0版本才加入的,如果使用的是低于这个版本的,这个方法并不会有效。

(2) 使用 never 类型

当函数返回一个错误或者不可能有返回值的时候,返回值类型为 never。所以可以给 switch 添加一个 default 流程,当前面的 case 都不符合的时候,会执行 default 后的逻辑:

  1. function assertNever(value: never): never {
  2. throw new Error("Unexpected object: " + value);
  3. }
  4. function getArea(s: Shape) {
  5. switch (s.kind) {
  6. case "square":
  7. return s.size * s.size;
  8. case "rectangle":
  9. return s.height * s.width;
  10. case "circle":
  11. return Math.PI * s.radius ** 2;
  12. default:
  13. return assertNever(s); // error 类型“Triangle”的参数不能赋给类型“never”的参数
  14. }
  15. }

采用这种方式,需要定义一个额外的 asserNever 函数,但是这种方式不仅能够在编译阶段提示我们遗漏了判断条件,而且在运行时也会报错。

8. 类型别名

最后补充一个知识点:类型别名。

类型别名就是给一种类型起个新的名字,之后只要使用这个类型的地方,都可以用这个名字作为类型代替,但是它只是起了一个名字,并不是创建了一个新类型。

可以使用 type 关键字来定义类型别名:

  1. type some = boolean | string
  2. const b: some = true // ok
  3. const c: some = 'hello' // ok
  4. const d: some = 123 // 不能将类型“123”分配给类型“some”

类型别名也可以使用泛型:

  1. type PositionType<T> = { x: T; y: T };
  2. const position1: PositionType<number> = {
  3. x: 1,
  4. y: -1
  5. };
  6. const position2: PositionType<string> = {
  7. x: "right",
  8. y: "left"
  9. };

使用类型别名时也可以在属性中引用自己:

  1. type Child<T> = {
  2. current: T;
  3. child?: Child<T>;
  4. };
  5. let ccc: Child<string> = {
  6. current: "first",
  7. child: {
  8. // error
  9. current: "second",
  10. child: {
  11. current: "third",
  12. child: "test" // 这个地方不符合type,造成最外层child处报错
  13. }
  14. }
  15. };

但注意,只可以在对象属性中引用类型别名自己,不能直接使用,比如下面这样是不对的:

  1. type Child = Child[]; // error 类型别名“Child”循环引用自身

另外要注意,因为类型别名只是为其它类型起了个新名字来引用这个类型,所以当它为接口起别名时,不能使用 extendsimplements

接口和类型别名有时可以起到同样作用:

  1. type Alias = {
  2. num: number;
  3. };
  4. interface Interface {
  5. num: number;
  6. }
  7. let _alias: Alias = {
  8. num: 123
  9. };
  10. let _interface: Interface = {
  11. num: 321
  12. };
  13. _alias = _interface;

可以看到用类型别名和接口都可以定义一个只包含 num 属性的对象类型,而且类型是兼容的。

那应该如何区分类型别名和接口这两者呢?
interface 只能用于定义对象类型,而 type 的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围显然更加广泛。

但是interface也有其特定的用处:

  • interface 方式可以实现接口的 extends 和 implements
  • interface 可以实现接口合并声明

    type Alias = { num: number }
    interface Interface {

    1. num: number;

    }
    declare function aliased(arg: Alias): Alias;
    declare function interfaced(arg: Interface): Interface;

此外,接口创建了一个新的名字,可以在其它任何地方使用,类型别名并不创建新名字,比如,错误信息就不会使用别名(提示的还是原来的名字)。

那么什么时候用类型别名,什么时候用接口呢:

  • 接口: 当定义的类型要用于拓展,即使用 implements 等修饰符时,用接口。
  • 类型别名: 当无法通过接口,并且需要使用联合类型或元组类型,用类型别名。

发表评论

表情:
评论列表 (有 0 条评论,406人围观)

还没有评论,来说两句吧...

相关阅读