bosens-China/blog

TypeScript 类型收窄

bosens-China opened this issue · 0 comments

image-20220904141910530

写这篇文章是因为最近在写一个管理异步队列的库,但是有一些类型推导不太好写,例如上面的期待是

if (values.stauts === 'error') {
  // values.data to Error
}

后面查询了一些文档和资料就有了这篇文章。

收窄方式

在具体讲解上面如何实现之前,先介绍一下几种收窄对象的方式。

typeof 类型守卫

function padLeft(padding: number | string, input: string): string {
  throw new Error('Not implemented yet!');
}

如果在 padLeft 函数内调用不是 stirng 和 number 相同的方法,例如 toString、valueOf 会提示方法不存在。

之所以出现这样问题是因为 Typescript 并不知道 padding 到底是 number 还是 string,但是你可以通过 typeof 的显式告诉它的类型。

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === 'string') {
    return padding.charAt(0);
  }
  // 排除了string,剩下就是number
  return padding.toFixed(2);
}

条件语句

在使用条件语句,例如 if&&||! 等条件语句的时候也会缩小类型。

看一个例子

function printAll(strs: string | string[] | null) {
  //
}

按照上面 typeof 语句很自然就想到判断是不是 object 来完成数组区分

function printAll(strs: string | string[] | null) {
  if (typeof strs === 'object') {
    strs.forEach((item) => {});
    // Object is possibly 'null'.
    //'item' is declared but its value is never read.
  }
}

不过写到一半就会发现提示报错了,strs 在 object 的判断下依然可能为 null,回忆一下 typeof 的运算符可以得知这个是正常现象,null 的 typeof 返回值也是 object,这个是 JavaScript 语言设计的一个缺陷。

不过要怎么消除这个影响呢?这就用到这条件语句,可以通过判断 strs 是否存在显式排除 null 的存在

function printAll(strs: string | string[] | null) {
  if (typeof strs === 'object' && strs) {
    // object
    return strs;
    // 这里strs类型已经被排除object了
  } else if (typeof strs === 'string') {
    return strs;
  }
  // null
  return strs;
}

相等运算符和 switch 收窄

还可以使用 =====!=!== 运算符来完成收窄

interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number) {}

例如上面,如果想让 container 排除 null 和 undefined 就可以使用 === 来完成,下面是示例

interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number) {
  if (container.value === undefined) {
    return 0;
  }
  if (container.value === null) {
    return 1;
  }
  return container.value === factor;
}

通过 === 完成了 undefined 和 null 的收窄过程,这里顺便说下其实上面的写法可以简写成 == ,== 在判断 undefined 跟 null 之间时返回 true。

interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number) {
  if (container.value == undefined) {
    return 0;
  }

  return container.value === factor;
}

in 运算符

in 运算符用于判断一个属性是否存在对象上,在 TypeScript 同样可以使用 in 来完成收窄。

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    return animal.swim();
  }

  return animal.fly();
}

上面 move 通过指定 swim in animal 告诉 TypeScript,在 if 中类型就是 Fish,因为 Bird 中不存在这个属性。

使用 in 运算符收窄时,使用其他联合类型不存在的属性来完成对某一类型的收窄

instanceof

与 JavaScript 中一样,instanceof 也可以判断某个属性是否匹配对象。

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
  } else {
    console.log(x.toUpperCase());
  }
}

赋值

这里直接看例子

let x = Math.random() < 0.5 ? 10 : 'hello world!';
// let x: string | number
x = 1;

console.log(x);
// let x: number
x = 'goodbye!';

console.log(x);
// let x: string

不过注意,分配给上面的 x 一定要符合 x 最初的联合类型,也就是说给定 boolean 会报错。

类型谓词

在使用 lodash 类型的函数库,通常都会有 isObject,你可能很好奇怎么实现。

其实它们就是使用了类型谓词,下面动手实现一个 isObject

function isObject (value:any) : value is object {
    return value && typeof value === 'object'
}

之后在配合使用就可以完成类型收窄

function test(pet: object | null) {
  if (isObject(pet)) {
    return pet;
  } else {
    return {};
  }
}

interface 收窄

上面将收窄的方式列举了一番,下面就看下在 interface 中如何实现收窄,最开始已经说了,我们期待使用 if 条件语句可以通过 status 的不同来完成 data 的变化。

在 TypeScript 我们可以使用联合类型将两者通用部分区分

export interface ChangeError {
  status: 'error';
  data: Error;
}

export interface ChangeData<T> {
  status: 'success';
  data: T;
}

之后拼接一起

export type Change<T = any> = {
  index: number,
  progress: number,
  total: number,
} & (ChangeError | ChangeData<T>);

之后通过 TypeScript 的条件收窄就自动完成这一过程。

收窄类型的实用示例

收窄 + never 结合可以完成一个联合类型添加新对象不处理报错。

interface A {
  name: 'a';
}
interface B {
  name: 'b';
}

type Types = A | B;

function check(values: Types) {
  switch (values.name) {
    case 'a':
      return '';
    case 'b':
      return '';
    default:
      const _never: never = values;
      return _never;
  }
}

never 表示永远为空的类型,结合 switch 可以将对象类型收窄尽,这时对象的类型就是 never。

现在如果给 Types 添加新的联合类型 default 就会抛出错误

interface A {
  name: 'a';
}
interface B {
  name: 'b';
}

type Types = A | B | { name: 'c' };

function check(values: Types) {
  switch (values.name) {
    case 'a':
      return '';
    case 'b':
      return '';
    default:
      const _never: never = values;
      return _never;
  }
}