2-1-7 TS 解决组合类型的检查(窄化)
类型的窄化
- 窄化和类型守卫
- 真值窄化
- 相等性窄化
- `in` 操作符窄化
- `instanceof` 窄化
- 控制流分析
- 类型断言
- 判别的联合
- Never类型
TS中的类型是可以组合使用的。
联合和窄化
type Padding = number | stringfunction 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: booleanif (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 Guardpet.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") { // Narrowingreturn 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 neverreturn _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
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
