我们来写个 padLeft 函数:
function padLeft(padding: number | string, input: string): string {
// ...
}
他的功能为:如果 padding 是 number 类型,那么在 input 前增加 padding 个空格,如果 padding 是 string 类型,那么直接把 padding 加到 input 前面。我们先实现下 number 类型的逻辑:
function padLeft(padding: number | string, input: string) {
return " ".repeat(padding) + input;
}
repeat
调用处报错了,因为 repeat
参数要求是 number 类型,但现在 padding 是 number | string 类型。所以应该单独处理 string 类型和 number 类型的情况。
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
这种处理看起来就像是无聊的 JavaScript 代码,但这就是重点,因为 TypeScript 即使添加了类型,但也没有改变 JavaScript 的代码编写方式。
也许这段代码看起来 TypeScript 也没做太多事,但事实上它做了很多,它将类型分析覆盖在 JavaScript 的运行时控制流构造上,如 if/else
、三元控制、循环、真实性检查等,这些都可能影响类型。
在上段代码的 if 检查中,TypeScript 看到 typeof padding === "number"
并将其理解为一种称为类型保护的特殊形式的代码。它会根据静态代码来推断类型,这种方式叫做类型范围缩小(Narrowing),如 if 语句块内,padding 的类型就是 number,这甚至可以在编辑器中看到:
TypeScript 有几种不同的方式能做到类型范围缩小。
typeof 类型检查
JavaScript 支持 typeof
运算符,它可以提供运行时值类型的基本信息,并返回一个字符串结果:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
TypeScript 可以通过如上结果识别类型,并且它还知道 JavaScript 的特殊情况(typeof null 的值是 object)
真值类型检查
真值是在 JavaScript 中经常听到的东西,可以在条件表达式、 &&
s、 ||
s、 if
语句、布尔否定 ( !
) 等中使用。
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
numUsersOnline 的类型不是 boolean,这里也不会报错,因为 JavaScript 会把值强制转换成 boolean,下面的值在布尔转换时为 false:
0
、0n
(bigint
类型的 0)NaN
""
(空字符串)null
undefined
你也可以通过 Boolean
函数或 !!
强制转换成布尔。如下代码:
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);
}
}
if (strs &&
这里去除掉了 strs 为 null 的情况,但你这样使用时也要注意:
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);
}
}
}
如果 if (strs)
被放到了顶层,那么会过滤掉 null 和 空字符串的情况,通常过滤掉空字符串不是期望的,所以要谨慎。
相等类型检查
TypeScript 还使用 switch
语句和等式检查(如 ===
、 !==
、 ==
和 !=
)来缩小类型范围。例如:
x 的类型是 string | number,y 的类型是 string | boolean,如果 x 等于 y,那么只有一种可能,那就是他们都是 string 类型,所以 if 语句块内可以使用 string 的方法。
直接对于特殊值判断相等或不等也能使类型范围缩小
strs !== null
就排除了 null 类型,typeof strs === "object"
也能判断出是数组。
对于双等于 null 这种也能正确判断出类型(在 JavaScript 中,双等于 null 代表这个值可能是 null 或 undefined,当然双等于 undefined 也是同样的)。
in 运算符
JavaScript 使用 in
运算符来确定对象或其原型链上是否具有某个属性。 TypeScript 将这一点作为类型范围缩小的一种方式。
例如,使用代码: "value" in x
。其中 "value"
是字符串文字, x
是联合类型。 “true”分支代表 x
具有 value
属性,“false”分支反之。
再看个例子:
instanceof
JavaScript 使用 instanceof 操作符检查是否为类的实例,更详细说:x instanceof Foo
会检查 x 的原型链上是否有 Foo.prototype
。所以这也是类型范围缩小的一种方式
赋值
在赋值时,TypeScript 推断右边类型,并设置给左边
后面代码赋值给 number 或是 string 都是合法的,但如果赋值 boolean 就会报错
控制流分析
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
if 块里面 padding 是 number 类型,外面 padding 是 string 类型,padLeft 返回值是 string 类型。
不同的控制流可以使类型不断改变:
类型谓词
到目前为止,我们使用现有的 JavaScript 结构来处理类型范围缩小,但有时您希望更直接地控制类型。
这里可以用到一个返回类型为类型谓词的函数:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
pet is Fish
是本例中的类型谓词。谓词采用 parameterName is Type
形式,其中 parameterName
是当前函数签名中参数的名称。
任何时候调用 isFish
时,TypeScript 都会将参数变量类型缩小为 Fish。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// ---cut---
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
请注意,TypeScript 不仅知道 if 中的 pet 是 Fish 类型,它还知道 else 中的 pet 不是 Fish 类型,那么就一定是 Bird 类型。
还可以通过 isFish 来过滤出 Fish 类型数组:
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "sharkey") return false;
return isFish(pet);
});
可区分联合
大多数时候在 JavaScript 中我们会处理稍微复杂的类型结构。
出于某种目的,假设我们正在尝试对圆形和正方形等形状进行编码。圆形记录它们的半径,正方形记录它们的边长。我们将使用一个名为 kind
的字段来判断我们正在处理的形状。我们尝试定义 Shape
类型。
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
我们使用字符串文字类型的联合: “circle” 和 “square” 来告诉我们应该将形状视为圆形还是正方形。通过使用 “circle” | “square” 而不是 string ,可以避免拼写错误问题。
我们编写一个 getArea
函数,根据它处理的是圆形还是方形来应用正确的逻辑。我们将首先尝试处理圆形。
在 strictNullChecks
下给我们一个错误,这是合理的,因为 radius
可能未定义。但是如果我们对 kind
属性执行适当的检查呢?
嗯,TypeScript 仍然不知道在这里做什么。但是我们已经达到了比类型检查器更了解值类型的地步。我们可以尝试使用非空断言( shape.radius
之后的 !
)来说明 radius
肯定存在。
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}
但这并不理想。我们不得不用那些非空断言 ( !
) 对类型检查器强制提醒,以说服它 shape.radius
已定义,但如果我们需要移动代码,这些断言很容易出错。此外,在 strictNullChecks
之外,我们无论如何都可以意外访问这些字段中的任何一个(因为可选属性只是假定在读取它们时始终存在)。我们可以做得更好。
当前 Shape
定义的问题是类型检查器无法根据 kind
属性知道是否存在 radius
或 sideLength
。我们需要将信息传达给类型检查器。考虑到这一点,我们重新定义 Shape
。
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
在这里,我们正确地将 Shape 分成了两种类型, kind 属性具有不同的值,但是 radius 和 sideLength 在它们各自的类型中被声明为必需的属性。
我们再次尝试检查 kind
属性
没有报错。
在这种情况下, kind
是公共属性(这被认为是 Shape
的判别属性)。检查 kind
属性是否为 "circle"
删除了 Shape
中不具有 "circle"
类型的 kind
属性的所有类型。这将 shape
缩小为 Circle
类型。
同样的检查也适用于 switch
语句。现在我们可以尝试编写完整的 getArea
,而不用任何讨厌的 !
非空断言。
这里最重要的是 Shape
的定义方式。向 TypeScript 传达了正确的信息( Circle
和 Square
实际上是具有特定 kind
字段的两个独立类型)。这样做可以让我们编写类型安全的 TypeScript 代码。
可区分联合不仅仅用于谈论圆和正方形。它们适用于在 JavaScript 中表示任何类型的消息传递方案,例如通过网络发送消息(客户端/服务器通信)。
never 类型
在类型范围缩小时,您可以将并集的选项减少到已经删除所有可能性并且什么都没有留下的程度。在这种情况下,TypeScript 将使用 never
类型来表示这种没有任何类型的类型。
详尽检查
never
类型可分配给每个类型;但是,没有类型可以分配给 never
( never
本身除外)。这意味着您可以使用范围类型缩小并依靠 never
出现在 switch
语句中进行详尽检查。
例如,向我们的 getArea
函数添加一个 default
分支,并分配 never
类型,那么未处理所有可能的情况时将引发错误。
将新成员添加到 Shape
联合会导致 TypeScript 错误: