类型缩小的概念
前面我们写了一些这样的代码:
function padLeft(padding: number | string, input: string): string {
if (typeof padding === 'number') {
return ' '.repeat(padding) + input
}
return padding + input
}
没有if判断时,无法执行语句return ’ '.repeat(padding) + input,使用if判读之后就可以执行了,这就是一个TypeScript类型缩小。
JavaScript 的运行时控制流结构(如 if/else、条件三元组、循环、真值检查等)都会影响TypeScript对这些类型的判断。换句话说,TypeScript会对代码语句进行检查,遵循我们的程序采用可能执行路径得出的更具体的可能类型。
typeof 类型保护
typeof一般返回的类型有:string、number、boolean、object、undefined、function、bigInt、symbol。TypeScript可以理解这些类型的缩小。
注意:在JavaScript中,null也算object类型。
在下面的例子中,我们企图先判断strs是否为对象类型,如果是的话就直接遍历,但是忘了string[]和null在JavaScript中都算object类型,故进入第一个判断的参数可能是string[]也可能是null类型,而null类型无法进行遍历,故会报错。
function printAll(strs: string | string[] | null) {
if (typeof strs === 'object') {
for (const s of strs) {//error strs可能是null类型
console.log(s)
}
} else if (typeof strs === 'string') {
console.log(strs)
} else {
}
}
真值缩小
if 这样的构造首先可以将条件、&&、||、if 语句、布尔否定 (!) 等 “强制转换” 到 boolean 来理解它们,然后根据结果是 true 还是 false 来选择它们的分支。
例如:0、NaN、0n(bigInt版本的0)、null、“ ”、undefined都会被强制转换为false。其他值都会被转换为true。
以上是推断为类型缩小的字面布尔类型true、false。你也可以直接使用Boolean函数来将值强制为Boolean类型。
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true !!会将一个值转换为其对应的字面布尔类型。
在if中使用条件来进行真值缩小非常常见。例如上面那个例子,我们只要在if语句中判断strs是否为null并上类型为object,就能将参数判断为数组。
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === 'object') {
for (const s of strs) {
console.log(s)
}
} else if (typeof strs === 'string') {
console.log(strs)
} else {
}
也可以先判断strs是否为null,再判断它是string类型还是object类型(string[])。
function printAll(strs: string | string[] | null) {
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
带有否定分支的话就从否定分支中过滤掉。
function multiplyAll(values: number[] | undefined, factor: number): number[] | undefined {
if (!values) {
return values
} else {
return values.map((x) => x * factor)
}
}
相等性缩小 使用switch !== === != ==
我们上面所举的printAll的例子没有处理str等于null的情况。相等性缩小可以处理。
function printAll(str: string | string[] | null) {
if (str !== null) {
if (str === 'string') {
console.log(str)
} else if (str === 'object') {
for (const s of str) {
console.log(s)
}
}
}
}
JavaScript 对 == 和 != 的更宽松的相等性检查也正确地缩小了类型。
检查某物 == null 是否实际上不仅检查它是否是值 null,它还检查它是否可能是 undefined。这同样适用于 == undefined。
注意下面示例使用的是!=号,可以同时将null和undefined的情况过掉。
interface Container {
value: number | null | undefined
}
function test(container: Container) {
if (container.value != null) {
console.log(container.value)
} else {
console.log('container.value可能是null或undefined')
}
}
in运算符缩小
使用代码:“value” in x。其中 “value” 是字符串字面,x 是联合类型。判断联合类型中是否含属性名为value的类型。
type Fish = { swim: () => void }
type Bird = { fly: () => void }
function move(animal: Fish | Bird) {
if ('swim' in animal) {
return animal.swim()
}
return animal.fly()
}
其中属性名必须带引号,函数可以是箭头函数也可以是普通函数。
可选属性存在于两侧进行缩小。这样的类型判断和我们写c语言的逻辑是一样的,符合要求的类型就会进入if判断中。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; (parameter) animal: Fish | Human
} else {
animal; (parameter) animal: Bird | Human
}
}
instanceof缩小
在 JavaScript 中,x instanceof Foo 检查 x 的原型链是否包含 Foo.prototype。
function logValue(x: Date | string) {
if (x instanceof Date) {//判断x中是否含有Date原型对象
x.toLocaleDateString()//Date的方法
} else {
x.toLocaleUpperCase()//string的方法
}
}
赋值
变量被赋值之后类型可能会缩小。
给变量x定义string|number类型。
对变量重新赋值一个1,此时中间的x还是string|number类型,但是再打印x时,他就变成了number类型。
再次给x赋值一个字符串,第三个x仍然是string|number类型,但是第四个打印的x被缩小为string类型。
变量的声明类型为string|number,中途我们可以随意地将string或者number类型赋值给x,因为TypeScript始终以声明类型来判断变量的可赋值性。
控制流分析
当分析一个变量时,控制流可以一次又一次地分裂和重新合并,并且可以观察到该变量在每个点具有不同的类型。
function example() {
let x: string | number | boolean
x = Math.random() < 0.5 //let x: string | number | boolean
console.log(x) //let x: boolean
if (Math.random() < 0.5) {
x = 'hello' //let x: string | number | boolean
console.log(x) //let x: string
} else {
x = 100 //let x: string | number | boolean
console.log(x) //let x: number
}
return x //let x: string | number
}
类型谓词
interface Fish {
swim: () => void
}
interface Bird {
fly: () => void
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined//这里使用断言是为了避免ts警告,由于不知道pet到底是何种类型,这里还是做了!==判断
}
function getPet(): Fish | Bird {
if (Math.random() < 0.5) {
return { swim: () => {} }
}
return { fly: () => {} }
}
let pet = getPet()
if (isFish(pet)) {
pet.swim() //let pet: Fish
} else {
pet.fly() //let pet: Bird
}
类型谓词采用paramsName is Type的形式定义,其中paramsName一定是参数名称。
判别联合
先抛出一个问题:如果形状是圆形,就计算圆形的面积。如果形状是正方形,就计算正方形的面积。
我们想到第一种类型定义方法:
interface Shape {
kind: 'circle' | 'square'
radius?: number
sideLen?: number
}
function handleShape(shape: Shape) {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2 //“shape.radius”可能为“未定义”
} else {
return shape.sideLen ** 2 //“shape.sideLen”可能为“未定义”
}
}
这个时候ts会报错,报错也是正常的,因为radius和sideLen都是可选值,你在传一个shape时,只有kind必传,有可能两个可选值都没传递,有可能kind是circle时你传递了sideLen,有可能kind是square时你传递了radius,这都会导致你无法计算面积,而ts也能推断这些。
我们可以把圆形和方形分开定义,但是给他们设置一个公共属性,让ts能够识别它属于什么形状。
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLen: number
}
type Shape = Circle | Square
function handleShape1(shape: Shape) {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2//(parameter) shape: Circle
} else {
return shape.sideLen ** 2//(parameter) shape: Square
}
}
//也可以使用switch语句
function handleShape2(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.sideLen ** 2
}
}
never类型
never 类型表示永远不会出现的值的类型。通常情况下,never 类型用于表示抛出异常或者永远不会结束的函数的返回类型。例如,一个函数如果抛出异常,它的返回类型就可以定义为 never
穷举检查
never 类型可分配给每个类型;但是,没有类型可分配给 never(never 本身除外)
例如:switch语句中,当所有可能情况处理判断后,shape到了default分支,就可以将shape赋值给never类型的变量。
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLen: number
}
type Shape = Circle | Square
function handleShape(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.sideLen ** 2
default:
const _exhaustiveCheck: never = shape
return _exhaustiveCheck
}
}
但是,ts不允许可能出现的情况,未经判断,就赋值给never类型的变量,下面的代码会报错。
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLen: number
}
interface Triangle {
kind: 'triangle'
sideLen: number
}
type Shape = Circle | Square | Triangle
function handleShape(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.sideLen ** 2
default:
const _exhaustiveCheck: never = shape//error:不能将类型“Triangle”分配给类型“never”
return _exhaustiveCheck
}
}