2-1-7 TS 解决组合类型的检查(窄化)

Posted 沿着路走到底

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-1-7 TS 解决组合类型的检查(窄化)相关的知识,希望对你有一定的参考价值。

类型的窄化

- 窄化和类型守卫

- 真值窄化

- 相等性窄化

- `in` 操作符窄化

- `instanceof` 窄化

- 控制流分析

- 类型断言

- 判别的联合

- Never类型

TS中的类型是可以组合使用的。

联合和窄化

type Padding = number | string

function padLeft(padding : Padding, input : string) : string 
    //...

但是这样会遇到一个问题,接下来需要用`typeof` 判断`padding` 的类型。

当然一个是`number|string` 的类型可以赋值成`number` 或者`string`

let x :number|string = 1
x = "Hello"

如果不判断:

function padLeft(padding: number | string, input: string) 
  return new Array(padding + 1).join(" ") + input;
  // Operator '+' cannot be applied to types 'string | number' and 'number'.

于是增加`typeof` 的判断:

function padLeft(padding: number | string, input: string) 
  if (typeof padding === "number") 
    return new Array(padding + 1).join(" ") + input;
  
  return padding + input;

当进行了`if + typeof` 操作后,ts可以识别变窄后的类型,称为窄化(Narrowing)。上面Narrowing的能力,让TS清楚的知道`padding` 是数字孩还是字符串。

在实现层面,TS会认为`typeof padding === "number"` 这样的表达式是一种类型守卫(type guard)表达式。当然这是纯粹实现层面的概念,准确来说`if + type guard` 实现了Narrowing。

**划重点:类型窄化(Type Narrowing)根据类型守卫(Type Guard)在子语句块重新定义了更具体的类型。**

typeof 的守卫们

"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"

注意:`typeof null === 'object'`

因此:

function printAll(strs: string | string[] | null) 
  if (typeof strs === "object") 
    for (const s of strs) 
		  //Object is possibly 'null'.
      console.log(s);
    
   else if (typeof strs === "string") 
    console.log(strs);
   else 
    // do nothing
  

真值窄化(Truthiness narrowing)

javascript有一张复杂的真值表,总结下来这些值都会拥有false的行为:

0
NaN
"" (the empty string)
0n (the bigint version of zero)
null
undefined

我们也可以通过真值实现窄化:

比如避免:TypeError: null is not iterable 错误。

if (strs && typeof strs === "object") 
    for (const s of strs) 
        console.log(s);
    
 

再举个例子:

function multiplyAll(
  values: number[] | undefined,
  factor: number
): number[] | undefined 
  if (!values) 
    return values;
   else 
    return values.map((x) => x * factor);
  

**划重点:真值(Truthiness narrowing)窄化帮助我们更好的应对null/undefined/0等值。**

相等性窄化

在窄化当中有一类隐式的窄化方法,就是相等性窄化。`===`, `!==`, `==`, and `!=` 都可以用来窄化类型。

举例:

function example(x: string | number, y: string | boolean) 
  if (x === y) 
		// x is string
   else 
    // x is string | number,
    // y is string | boolean
  

再看一个例子:

function printAll(strs: string | string[] | null) 
  if (strs !== null) 
    if (typeof strs === "object") 
      for (const s of strs) 
                       
          //(parameter) strs: string[]

      
     else if (typeof strs === "string") 
      
          // (parameter) strs: string
    
  

考考你:

interface Container 
  value: number | null | undefined;


function multiplyValue(container: Container, factor: number) 
  if (container.value != null) 
		// container.value是什么类型?
    container.value *= factor;    // number 类型
  

`in` 操作符窄化

回忆一下:JS中的`in` 操作符的作用是?

——检验对象中是否有属性。

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


function move(animal: Fish | Bird) 
  if ("swim" in animal) 
    return animal.swim();
  

  return animal.fly();

特别提一下,为什么不用`instanceof Fish` ? 因为`type` 没有运行时。

type 只是别名,并没有实例。

`instanceof` 窄化

`instanceof` 可以窄化,注意Date不能是`type` 而是真实存在的Function类型。

function logValue(x: Date | string) 
  if (x instanceof Date) 
		// x is Date
   else 
    // x is string
  

组合类型推导

有时候Typescript会推导出组合类型。

let x = Math.random() < 0.5 ? 10 : "hello world!";

这个时候x是`number | string`

当然, 这里有个问题是`number|string` 的类型可以赋值成`number` 或者`string` 。

控制流分析

Typescript怎么做到窄化的?

首先在语法分析阶段,Typescript的编译器会识别出类型卫兵表达式。包括一些隐性的类型卫兵,比如真值表达式、instanceof等等。

那么在语义分析的时候,Typescript遇到控制流关键字`if/while` 等,就会看看这里有没有需要分析的窄化操作。

例如:

function padLeft(padding: number | string, input: string) 
  if (typeof padding === "number") 
    return new Array(padding + 1).join(" ") + input;
  
  return padding + input;

- 首先TS会看到有一个卫兵表达式:`typeof padding==='number'`

- 然后TS会对返回值`return padding+input` 以及`return new` 分别做窄化

- 窄化的本质是重新定义类型

当然很多语句都会触发窄化:

function example() 
  let x: string | number | boolean;

  x = Math.random() < 0.5;
	//x: boolean

  if (Math.random() < 0.5) 
    x = "hello";         
		//x: string
   else 
    x = 100;
    // x : number         
  

  return x;        
  // x: string | number

类型断言(Type Assertions/Predicate)

Assertion和predicate翻译过来都是断言。在计算机中,Asssertion通常是断言某个表达式的值是不是true/false。Assertion在很多的测试库中被使用,比如`assert.equals(a, 1)` 。从语义上,这里在断言a的值是1(a===1是true)。

**划重点:Assertion在说某个东西是什么。**

Predicate通常是一个函数,返回值是true/false,比如说list.filter( x=>x.score > 500),`x=>x.score > 500` 这个函数是一个`predicate` 函数。

**划重点:Predicate是一个返回true/false的函数**。

TS中有两个断言操作符,`Assertion` 操作符`as` 和`predicate` 操作符`is` 。

`as` 操作符提示Typescript某种类型是什么(当用户比Typescript更了解类型的时候使用)。`is` 操作符是用户自定义的类型守卫,用于帮助Typescript Narrowing。

具体的例子:

function isFish(pet: Fish | Bird): pet is Fish  
  return (pet as Fish).swim !== undefined;


let pet = 
    fly : () => 


if (isFish(pet))  // isFish(pet)成为了Type Guard
  pet.swim();
 else 
  pet.fly();

不加`pet is Fish` 会报错, 因为函数内代码没有做窄化。

判别的联合(Discriminated unions)

考虑这个定义:

interface Shape 
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;


function getArea(shape: Shape) 
  return Math.PI * shape.radius ** 2;

会报错

function getArea(shape: Shape) 
  if (shape.kind === "circle") 
    return Math.PI * shape.radius ** 2;
    // Object is possibly 'undefined'.
  
 

于是用非Null断言操作符`!`

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

问题在于`circle` 应该是一种单独的类型,Shape可能还有`rect` 等。

解决方案:

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


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


type Shape = Circle | Square;

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

整理下:

function getArea(shape: Shape) 
  switch (shape.kind) 
    case "circle":
      return Math.PI * shape.radius ** 2;
                       
    case "square":
      return shape.sideLength ** 2;
             
  

Never类型

Never,就是不应该出现的意思。Never类型代表一个不应该出现的类型。因此对Never的赋值,都会报错。

比如下面处理default逻辑:

function getArea(shape: Shape) 
  switch (shape.kind) 
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      // Type ... is not assignable to type never
      return _exhaustiveCheck;
  

然后我们增加一个`triangle` :

interface Triangle 
  kind: "triangle";
  sideLength: number;


type Shape = Circle | Square | Triangle;

这个时候因为没有实现Triangle的getArea,因此会报错:`Type 'Triangle' is not assignable to type 'never'.`

never 类型不允许赋值,给 never 类型 赋值,会报错,相当于 throw 了一个异常,说明程序不希望走到这一行。

1

以上是关于2-1-7 TS 解决组合类型的检查(窄化)的主要内容,如果未能解决你的问题,请参考以下文章

vite vue3项目打包,跳过ts检查

从 Prolog 到 Haskell 的思考——生成真值组合列表

默认值+TS类型约束提高数据处理成功率

组合电路(反推逻辑表达式,组合电路的控制结果只和输入变量的状态有关)

如何从 firestore 检查有多少项目具有真值或假值并在列表中仅显示真值? - 颤动

检查Angular HTML中的布尔真值