TS 中的类型运算:联合类型

TS = JS + 类型系统

TS = JS + 类型系统

JS 可以对值进行加减运算,如果把 TS 的类型系统当作一门语言,TS 也同样可以对类型进行各种运算

联合类型(并集)

英文全称:union types

type A = { name: string } 表示 name 为 string 的所有对象 但不要错误地认为这些对象只有 name 这一个 key 比如对象 { name: 'ClariS'; age: 18 } 也是属于 A 类型的 即 A 类型的对象可以有 age,也可以没有 age 同理,type = { age: number } 表示 age 为 number 的对象,这些对象的 name 可以为空,也可以不为空

如何使用联合类型?

ts
const f1 = (a: number | string) => {
  '既不能把 a 当作 number'
  '也不能把 a 当做 string'
  '那么,怎么使用 a 变量呢?'
}

如果不拆开类型,就只能使用 number 和 string 共同拥有的方法或属性,比如 toString()

因此要使用联合类型,就得先区分类型,也就是进行类型收窄(Narrowing)

如何进行类型收窄?

使用 typeof

ts
const f1 = (a: number | string) => {
  if (typeof a === 'number') {
    a.toFixed(2);
  } else if (typeof a === 'string') {
    parseFloat(a).toFixed(2);
  } else {
    throw new Error('Never do this');
  }
};

typeof 的局限性:无法区分数组、日期、普通对象以及 null(返回值都是 "object")

使用 instanceof

ts
const f1 = (a: Array<Date> | Date) => {
  if (a instanceof Date) {
    a.toISOString();
  } else if (a instanceof Array) {
    a[0].toISOString();
  } else {
    throw new Error('Never do this');
  }
};

instanceof 的局限性:

  • 不支持基础类型,string、number、bigint、boolean、symbol、undefined
  • 不支持 TS 独有的类型
    ts
    type Person = { name: string };
    
    const f1 = (a: Person | Person[]) => {
      if (a instanceof Person) {
        //             ^--- 报错:type 不能用作 value
      } else {
        throw new Error('Never do this');
      }
    };
    

使用 in 操作符

ts
type Person = { name: string };

const f1 = (a: Person | Person[]) => {
  if ('name' in a) {
    a; // Person
  } else {
    a; // Person[]
  }
};

in 操作符的局限性:只适用于部分对象

比如当判断的两个对象存在相同的 key (存在相同 key,其中一个对象的 key 可能不存在),或不为键值对形式的对象(比如日期、正则、函数等)时,则无法使用 in 操作符来收窄类型

使用 JS 中判断类型的函数

ts
const f1 = (a: string | string[]) => {
  if (Array.isArray(a)) {
    a.join('\n').toString();
  } else if (typeof a === 'string') {
    parseFloat(a).toFixed(2);
  } else {
    throw new Error('Never do this');
  }
};

使用逻辑

ts
const f1 = (a?: string[]) => {
  if (a) {
    a; // string[]
  } else {
    a; // undefined
  }
};

const f2 = (a: string | number) => {
  a = 1;
  a; // number
};

const f3 = (x: string | number, y: string | boolean) => {
  if (x === y) {
    x; // string
    y; // string
  } else {
    x; // string | number
    y; // string | number
  }
};

使用类型谓词 is

上述所有的类型收窄方法都是通过 JavaScript 来实现的,有没有区分类型的万全之法?

ts
type Rect = { height: number; width: number };

type Circle = { center: [number, number]; radius: number };

// 推荐使用普通函数实现 isRect,而不使用箭头函数
function isRect(x: Rect | Circle): x is Rect {
  return 'height' in x && 'width' in x;
}

const f1 = (a: Rect | Circle) => {
  if (isRect(a)) { // `isRect(a)` 判断 a 是不是 Rect
    a; // Rect
  } else {
    a; // Circle
  }
};

优点:支持所有 TS 类型 缺点:需要自己实现,挺麻烦的

有没有更简单的办法?

可辨别联合

ts
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }
type Shape = Circle | Square

const f1 = (shape: Shape) => {
  if (shape.kind === 'circle') {
    shape // Circle
  } else if (shape.kind === 'square') {
    shape // Square
  } else {
    shape // never
  }
}

让复杂类型的收窄变成简单类型的对比

对于类型 T = A | B | C | D | ...,有以下要求:

  1. A、B、C、D... 有相同属性 kind 或其他
  2. kind 的类型是简单类型
  3. 各类型中的 kind 可区分

则称 T 为可辨别联合

一句话总结:具有同名、可辨别的简单类型的 key 的联合类型,称为可辨别联合

使用断言

可以使用 as 进行强制的类型收缩

ts
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }
type Shape = Circle | Square

const f1 = (shape: Shape) => {
  (shape as Circle).radius = 4
}

any 等于所有类型的联合吗?

这里直接给出结论:any 不等于所有类型的联合

注意:这里的所有类型不包括 never unknown any void

用反证法可以证明:

  1. 只要类型发生了联合,就只能使用它们共同拥有的属性或方法(想要使用各自的方法就必须做类型收窄)
  2. 但是使用 any 之后,不做类型收窄可以使用所有类型的方法
  3. 说明 any 不是所有类型的联合
ts
const f1 = (a: string | number) => {
  a.toString();
  a.toFixed(); // 报错
  a.split(); // 报错
};

const f2 = (a: any) => {
  a.toString();
  a.toFixed();
  a.split();
};

且 ts 的绝大部分规则对 any 不生效,但有一种特殊情况,any 无法赋值给 never

那什么等于所有类型的联合呢?

ts
const f1 = (a: unknown) => {
  if (a instanceof Date) {
    a; // Date
  }
};

type Rect = { height: number; width: number };
type Circle = { center: [number, number]; radius: number };
function isRect(x: Rect | Circle): x is Rect {
  return 'height' in x && 'width' in x;
}

const f2 = (a: unknown) => {
  if (isRect(a)) {
    a; // Rect
  }
};

可以看出,unknown 是可以收窄到任何类型的,因此,unknown 就是所有类型的联合

TS 中的类型运算:交叉类型
TypeScript 中的类型(下)

评论区

评论加载中...