ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

07 - 解决组合类型的检查(窄化)

2022-03-08 19:04:25  阅读:196  来源: 互联网

标签:return 07 组合 number padding shape 窄化 string


# 类型的窄化



这节课我们的重点:

- 窄化和类型守卫
- 真值窄化
- 相等性窄化
- `in` 操作符窄化
- `instanceof` 窄化
- 控制流分析
- 类型断言
- 判别的联合
- Never类型



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



## 联合和窄化

比如:

```ts
type Padding = number | string

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

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

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

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





如果不判断:

```ts
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` 的判断:

```tsx
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)在子语句块重新定义了更具体的类型。**

所以,我们学这干嘛?TS比JS有进步吗?



### typeof 的守卫们



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

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

因此:

```ts
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的行为:

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

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



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

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

再举个例子:

```tsx
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 `!=` 都可以用来窄化类型。

举例:

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

再看一个例子:

```tsx
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
}
}
}
```

考考你:

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

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



## `in` 操作符窄化



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

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

```tsx
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` 没有运行时。



## `instanceof` 窄化



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

```ts
function logValue(x: Date | string) {
if (x instanceof Date) {
// x is Date
} else {
// x is string
}
}
```



## 组合类型推导

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

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

这个时候x是`number | string`

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



## 控制流分析

你可能会问:Typescript怎么做到窄化的?

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

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

```ts
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` 分别做窄化
- 窄化的本质是重新定义类型

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

```tsx
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。

具体的例子:

```ts
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` 会怎样?

思考:as/is符不符合计算机标准语言中Assertion/Predicate的含义?



## 判别的联合(Discriminated unions)



考虑这个定义:

```tsx
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}

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

有什么问题吗?如果这样呢?

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

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

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

舒服!???——还没有——

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

解决方案:

```ts
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;
}
}
```

整理下:

```tsx
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逻辑:

```ts
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` :

```ts
interface Triangle {
kind: "triangle";
sideLength: number;
}

type Shape = Circle | Square | Triangle;

```

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



## 总结

思考:窄化解决了什么问题?——联合类型在使用中根据不同控制条件重定义的问题吗?——更提升对联合类型校验的问题。

思考:`in` `typeof` `instanceof` 中有没有遇到JS中没有的关键字?所以结论是什么?——TS是JS的超集,但是TS会尽量避免新增特性。`as` `is` `keyof` `enum`在JS中没有。

标签:return,07,组合,number,padding,shape,窄化,string
来源: https://www.cnblogs.com/anans/p/15981962.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有