当一个值的类型是联合类型时,对该值执行某个类型特有的方法时typescript会直接抛出警告,如下:
function toFixed(target: number | string, fractionDigits?: number) {
target.toFixed(fractionDigits) // Property 'toFixed' does not exist on type 'string | number'.
}
由于toFixed方法只存在于number类型上,而此时typescript无法明确的知道target是number类型还是string类型。因此会抛出警告,而要解决这个问题,就必须要让typescript明确的知道target是number类型;这个时候就需要用到类型窄化,如下:
function toFixed(target: number | string, fractionDigits?: number) {
if (typeof target === 'number') {
target.toFixed(fractionDigits)
}
// do nothing
}
抛开类型注释不看,这段代码也经常会出现javascript代码中,在javascript代码中这么写是为了避免运行时代码出错。同样的,typescript也能理解typeof操作符的含义,通过typeof target === 'number'
typescript能够明确的知道在这个if分支下,target的类型被锁定为number类型,从而避免了静态类型检查抛出警告,这就是类型窄化其中的一种。
类型窄化分为好几种,下面我们将一种一种的详细探究。
typeof 类型守卫(type guards)
typeof操作符在typescript的行为中与在javascript中的行为并无二致。它会返回一组特定的字符串
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
真值窄化(Truthiness narrowing)
当我们对一个值为null或者undefined变量进行读取属性时,javascript运行时会抛出异常,而typescript也会抛出警告,如下:
function getName(target: { name: string, } | null | undefined) {
return target.name // Object is possibly 'null' or 'undefined'.
}
要移除这个警告就要用到真值窄化,如下:
function getName(target: { name: string, } | null | undefined) {
if (target) {
return target.name
}
return undefined
}
这样的代码在javascript中我们也经常写,typescript能够像明白typeof操作符一样,同样也能明白if条件操作符的含义。
等性窄化(Equality narrowing)
function example(x: number | string, y: string | boolean) {
if (x === y) {
x.toUpperCase()
y.toUpperCase()
} else {
// do nothing
}
}
如上代码所示,x为number或者string类型,y为string或者boolean类型。当通过全等号判断他们相等时,typescript就能明确的知道x和y都是string类型。因为在javascript中全等号两个变量是否相等,首先他们的类型得一致,否则将直接判断为不相等。 上面讲到的真值窄化案例也可以通过等性窄化来解决,如下
function getName(target: { name: string, } | null | undefined) {
if (target != null) {
return target.name
}
return undefined
}
in
操作符窄化
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
javascript中有一种用来判断某个对象是否含有某个属性的操作符:in
操作符,typescript也将其纳入类型窄化方法中的一种。如上代码,通过"swim" in animal
判断typescript就能够明确的知道,animal是Fish类型还是Bird类型。
instanceof
操作符窄化
在javascript中,有一种判断某个值是否为另一个值的实例的操作符:instanceof
操作符;同样的,typescript也能明白这个操作符的含义,该操作符在面向对象编程中是十分有用的。示例如下:
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
赋值窄化
当你没有为某个变量提供明确的类型注释时,typescript可以通过对该变量的赋值自动推算出变量的类型,如下:
let bool = true
bool = 1 // Type 'number' is not assignable to type 'boolean'.
let x = Math.random() < 0.5 ? 10 : "hello world!"; // number | string
x = 1
x = '1'
x = true // Type 'boolean' is not assignable to type 'string | number'.
类型断言
除了上述提到的方法以外,还可以通过类型断言来进行类型窄化。 在typescript中断言分为两种:
- predicate
- assert
predicate
之前我们提到可以通过typeof来进行类型窄化,但是假如我们要将typeof value === 'number'
封装成函数,例如打造一个类似lodash的库,那么仅仅依靠typeof就不管用了,看下面的代码:
function isNumber(value: any) {
return typeof value === 'number'
}
function toFixed(target: number | string, fractionDigits?: number) {
if (isNumber(target)) {
target.toFixed(fractionDigits) // Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.
}
// do nothing
}
typescript只能推断出isNumber返回的是布尔值,但却无法明确的知道返回布尔值是否代表value是不是number类型。因此typescript依旧会抛出警告,要解决这个问题就需要用到type predicate
,如下:
function isNumber(value: any): value is number {
return typeof value === 'number'
}
function toFixed(target: number | string, fractionDigits?: number) {
if (isNumber(target)) {
target.toFixed(fractionDigits)
}
// do nothing
}
assert
type assert
的代码示例如下:
function toFixed(target: number | string, fractionDigits?: number) {
(target as number).toFixed(fractionDigits)
}
通过as
操作解决这个问题似乎省事很多,但其实这是一个危险的方案,尽量要避免使用。
never
类型
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
return shape
}
}
如上代码所示,当我们case少一个分支时,typescript是无法帮助我们预知道这个bug的存在。要解决这个问题就需要用到一个特殊的类型: never
。
never
的特点如下:
never
类型可以被赋值给任何类型,但是没有类型可以被赋值给never
类型 利用这个特点,我们可以对以上代码进行一些改动,来解决当Shape的联合类型变多而我们又忘了在switch中添加一个case分支时typescript不会抛出警告的问题,代码如下:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const res: never = shape // Type 'Triangle' is not assignable to type 'never'.
return res
}
}
由于漏了一个case分支,所以走到default分支时,shape的类型应该是Triangle。别忘了我们刚刚提到的never
类型的特点:没有类型可以被赋值给never
类型。所以此时typescript就会抛出警告,因此我们就知道修改代码时漏了加一个case分支,而不用等到运行程序进行测试的时候才发现问题。
总结
以上就是关于类型窄化的全部内容了,总的来说typescript保持了所有javascript既有的特性,即使这个特性在很多别的语言看来是比较怪异的。也正因如此,即使很多概念我们不了解,我们也会下意识的这么写,如:typeof类型守卫、真值窄化、等性窄化、in
操作符窄化、instanceof
操作符窄化、赋值窄化;也加入了一些新的概念,使得我们更容易的处理复杂的情况,如:type predicate/type assert/never
类型。