TypeScript 联合类型(union type)

TS是JS的超集,在JS的基础上添加了一套类型系统,这样的TS可以被静态分析带来的好处显而易见。

let val: string = 'val';

声明一个string类型的变量val

let val: string = 'val';
val = 1; // Type 'number' is not assignable to type 'string'.

因为number类型和string类型并不兼容,在string类型值出现的地方并不能使用number类型指完成替换,所以在TS世界中给string类型的val变量赋值number类型的值会报错。

但是在JS中并没有赋值的限制:

// javascript
var val = 'val';
b = 1; // 这里不会报错

在JS中变量val首先被赋值了字符串,然后被赋值了数字,这两个数据类型并不一致,但是因为JS没有静态类型检查,所以这并不会报错。

TS的联合类型可以适应这种情况,表示这个变量可能是类型a也可能是类型b也可能是类型c等类型。

let val: string | number = 'val'
val = 1 // 这里并不会报错

在TS中,这里声明的变量val则表示可能是类型string也可能是类型number,所以对变量赋值string类型值和number类型值,并不会报错。

但问题也随之而来。

function test(val: string | number) {
    
    
  val.toLowerCase() // Property 'toLowerCase' does not exist on type 'string | number'.
}

TS会提示类型 string | number 上没有属性toLowserCase。

这个报错也很容易理解,因为类型string的数据上可以调用方法toLowerCase,但是number不可以。因为变量val的类型,string和number都有可能,TS并不能确定val在运行时是string类型,所以会出现这个错误。

我们需要一个方法来告诉TS这个变量现在是string类型。

使用typeof缩小类型范围

对于上面例子中的变量val.toLowerCase的报错来说是因为val的类型范围有点大(string | number),如果我们通过某种方式缩小该范围为string,那么在val上访问属性toLowserCase应当没有问题。

function test(val: string | number) {
    
    
  if (typeof val === 'string') {
    
    
    val.toLowerCase() // 这里并不会报错
    console.log('val是字符串')
  } else {
    
    
    console.log('val是数字')
  }
}

在TS中,if中的typeof val === 'string'这种形式的代码会被识别为类型保护(type guard)。TS会分析代码的执行流程,缩小变量可能的类型。在分支if (typeof val === ‘string’) 中变量val的类型被TS识别为’string’,所以在这个分支下val调用string类型数据的方法并不会报错。

因为val是string和number这两个类型的联合,TS不仅知道if子句中的val是string类型,还知道else中的val是number类型。

可以被typeof进行类型保护的类型有:

  • “string”
  • “number”
  • “bigint”
  • “boolean”
  • “symbol”
  • “undefined”
  • “object”
  • “function”

使用in缩小类型范围

上面介绍了对于基础类型的识别,在TS中使用频率更多的还有对象,使用typeof来区别不同对象显然是有问题的,因为typeof出来的结果都是’object’无法分辨两个不同的对象。

type A = {
    
    a: string}
type B = {
    
    b: string}

function test(val: A | B) {
    
    
  val.a // Property 'a' does not exist on type 'A | B'
}

在test函数中无论是访问val.a还是val.b都会报错,而原因已经明白,TS无法确定变量val的具体类型,TS并不知道当前是类型A还是类型B,所以我们需要帮他一把。

type A = {
    
    a: string}
type B = {
    
    b: string}

function test(val: A | B) {
    
    
  if ('a' in val) {
    
    
    console.log(val.a)
    return
  }
  console.log(val.b)
}

其中的in操作同样会被TS识别为类型保护,如果属性a存在于变量val中那么就能识别出val变量是类型A,则可以正常访问类型A中存在的属性a。

使用instanceof缩小类型范围

对于对象类型的区分除了使用操作符in还可以使用instanceof来完成。

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

使用 === 和 == 缩小类型范围

使用严格等于(===)也可以在某些特别情况下正确缩小类型范围

function example(x: string | number, y: string | boolean) {
    
    
  if (x === y) {
    
    
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();

    y.toLowerCase();
  } else {
    
    
    console.log(x); // x 是 string | number

    console.log(y); // y 是 string | boolean
  }
}

在这个例子中xstringnumber的联合,而y是stringboolean的联合,当x === y的时候只可能xy都是string类型。所以在分支if (x === y) 中x和y的类型被正确识别为string类型。

我们知道在JS中宽松等于(== null)可以匹配null和undefined两种类型,当然TS也知道,所以 ==null,可以被用来识别类型。

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

function multiplyValue(container: Container, factor: number) {
    
    
  // Remove both 'null' and 'undefined' from the type.
  if (container.value != null) {
    
    
    console.log(container.value);
    // Now we can safely multiply 'container.value'.
    container.value *= factor;
  }
}

即使container.value可能是null或者undefined类型,但是在分支container.value != null中,该变量类型就只可能是number类型,所以其参与算术运算并不会报错。

区分联合类型

interface Circle {
    
    
  kind: "circle";
  radius: number;
}

interface Square {
    
    
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
    
    
  if (shape.kind === "circle") {
    
    
    return Math.PI * shape.radius ** 2;
  }
}

TS会通过参与联合的类型都有的属性kind来识别当前shape是Circle或者Square达到类型保护的目的。

类型谓词

上面介绍了TS对于联合类型中基础类型和对象类型的类型保护。但是这并不能覆盖全部的场景,例如上面介绍到的内容,并不能区分两个函数:

type fn1 = (arg: number) => boolean
type fn2 = (arg: string) => boolean

function test(fn: fn1 | fn2) {
    
    
  return fn(1)
}

在这个例子里,我们并没有方法来识别fn是类型fn1还是类型fn2。

在前面的例子里,例如 if (typeof a === ‘string’) 这里面的变量a会被TS类型系统识别为string,如果TS将识别一个变量为某个类型的能力开放给开发者,上面的问题就会迎刃而解。这个能力就是类型谓词。

通过观察上面类型保护的规律就会发现TS总会询问:变量a是类型A吗?被识别为类型保护的操作的回答总是true或者false都是boolean值。typoef a === ‘string’或者’a’ in A或者a instanceof A,这些操作的返回值都是boolean,并且都是和特定类型作比较。

// 我就是类型谓词形成的类型保护
function isTypefn1(fn: fn1 | fn2): fn is fn1 {
    
    
  if (fn.name === 'fn1') return true
  return false
}
function test(fn: fn1 | fn2) {
    
    
  if (isTypefn1(fn)) return fn(1)
  else return fn('1')
}

其中 isTypefn1调用的返回值就会告诉TS入参是不是类型fn1,这样TS就可以识别变量fn的类型完成类型保护。

参考

Narrowing

Guess you like

Origin blog.csdn.net/letterTiger/article/details/118032682