类型缩小

假设我们有一个名为 padLeft 的函数。

🌐 Imagine we have a function called padLeft.

ts
function padLeft(padding: number | string, input: string): string {
throw new Error("Not implemented yet!");
}
Try

如果 padding 是一个 number,它会将其视为我们想要在 input 前添加的空格数量。 如果 padding 是一个 string,它应该只在 input 前添加 padding。 让我们尝试实现当 padLeftpadding 传递 number 时的逻辑。

🌐 If padding is a number, it will treat that as the number of spaces we want to prepend to input. If padding is a string, it should just prepend padding to input. Let’s try to implement the logic for when padLeft is passed a number for padding.

ts
function padLeft(padding: number | string, input: string): string {
return " ".repeat(padding) + input;
Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.2345Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.
}
Try

糟糕,我们在 padding 上出现了一个错误。TypeScript 提示我们,将类型为 number | string 的值传递给 repeat 函数是不对的,因为该函数只接受 number,而 TypeScript 是正确的。换句话说,我们还没有明确检查 padding 是否为 number,也没有处理它是 string 的情况,所以我们就来做这件事吧。

🌐 Uh-oh, we’re getting an error on padding. TypeScript is warning us that we’re passing a value with type number | string to the repeat function, which only accepts a number, and it’s right. In other words, we haven’t explicitly checked if padding is a number first, nor are we handling the case where it’s a string, so let’s do exactly that.

ts
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Try

如果这看起来大多像普通的 JavaScript 代码,那就是重点所在。除了我们加上的注解,这段 TypeScript 代码看起来就像 JavaScript。其理念是,TypeScript 的类型系统旨在尽可能让编写典型的 JavaScript 代码变得简单,而不需要为了类型安全而费尽心思。

🌐 If this mostly looks like uninteresting JavaScript code, that’s sort of the point. Apart from the annotations we put in place, this TypeScript code looks like JavaScript. The idea is that TypeScript’s type system aims to make it as easy as possible to write typical JavaScript code without bending over backwards to get type safety.

虽然它看起来不太起眼,但实际上这里面有很多内容。就像 TypeScript 使用静态类型分析运行时值一样,它在 JavaScript 的运行时控制流结构上进行类型分析,如 if/else、条件三元运算符、循环、真假值检查等,这些都会影响到类型。

🌐 While it might not look like much, there’s actually a lot going on under the covers here. Much like how TypeScript analyzes runtime values using static types, it overlays type analysis on JavaScript’s runtime control flow constructs like if/else, conditional ternaries, loops, truthiness checks, etc., which can all affect those types.

在我们的 if 检查中,TypeScript 会看到 typeof padding === "number" 并将其理解为一种称为 类型保护 的特殊代码形式。TypeScript 会跟踪我们程序可能的执行路径,以分析特定位置上值的最具体类型。它会查看这些特殊检查(称为 类型保护)和赋值,而将类型细化为比声明更具体类型的过程称为 类型缩小。在许多编辑器中,我们可以观察到这些类型的变化,我们在示例中也会这样做。

🌐 Within our if check, TypeScript sees typeof padding === "number" and understands that as a special form of code called a type guard. TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing. In many editors we can observe these types as they change, and we’ll even do so in our examples.

ts
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
(parameter) padding: number
}
return padding + input;
(parameter) padding: string
}
Try

TypeScript 可以理解几种不同的结构来缩小类型。

🌐 There are a couple of different constructs TypeScript understands for narrowing.

typeof 类型保护

🌐 typeof type guards

正如我们所见,JavaScript 支持一个 typeof 操作符,它可以提供我们在运行时所拥有值的非常基本的类型信息。TypeScript 希望它返回特定的一组字符串:

🌐 As we’ve seen, JavaScript supports a typeof operator which can give very basic information about the type of values we have at runtime. TypeScript expects this to return a certain set of strings:

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

正如我们在 padLeft 中看到的,这个操作符在许多 JavaScript 库中相当常见,TypeScript 可以理解它,以在不同的分支中缩小类型。

🌐 Like we saw with padLeft, this operator comes up pretty often in a number of JavaScript libraries, and TypeScript can understand it to narrow types in different branches.

在 TypeScript 中,针对 typeof 返回的值进行检查是一种类型保护。因为 TypeScript 编码了 typeof 如何对不同的值进行操作,所以它了解 JavaScript 中的一些特性。比如,注意到在上面的列表中,typeof 并不会返回字符串 null。看看下面的示例:

🌐 In TypeScript, checking against the value returned by typeof is a type guard. Because TypeScript encodes how typeof operates on different values, it knows about some of its quirks in JavaScript. For example, notice that in the list above, typeof doesn’t return the string null. Check out the following example:

ts
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
'strs' is possibly 'null'.18047'strs' is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
Try

printAll 函数中,我们尝试检查 strs 是否是一个对象,以查看它是否是数组类型(现在可能是一个很好的时机来强调数组在 JavaScript 中是对象类型)。 但事实证明,在 JavaScript 中,typeof null 实际上是 "object"! 这是历史上一些不幸的意外之一。

🌐 In the printAll function, we try to check if strs is an object to see if it’s an array type (now might be a good time to reinforce that arrays are object types in JavaScript). But it turns out that in JavaScript, typeof null is actually "object"! This is one of those unfortunate accidents of history.

有足够经验的用户可能不会感到惊讶,但并不是每个人都在 JavaScript 中遇到过这种情况;幸运的是,TypeScript 会告诉我们 strs 只是被缩小到了 string[] | null,而不是直接缩小到 string[]

🌐 Users with enough experience might not be surprised, but not everyone has run into this in JavaScript; luckily, TypeScript lets us know that strs was only narrowed down to string[] | null instead of just string[].

这可能是一个很好的过渡,进入我们所称的“真实性”检查。

🌐 This might be a good segue into what we’ll call “truthiness” checking.

真值缩小

🌐 Truthiness narrowing

真值可能不是你在字典中可以找到的词,但你会在 JavaScript 中听到很多东西。

🌐 Truthiness might not be a word you’ll find in the dictionary, but it’s very much something you’ll hear about in JavaScript.

在 JavaScript 中,我们可以在条件语句、&&||if 语句、布尔取反(!)等中使用任意表达式。 例如,if 语句并不要求它们的条件总是具有 boolean 类型。

🌐 In JavaScript, we can use any expression in conditionals, &&s, ||s, if statements, Boolean negations (!), and more. As an example, if statements don’t expect their condition to always have the type boolean.

ts
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
Try

在 JavaScript 中,像 if 这样的结构首先会将它们的条件“强制转换”为 boolean,以便理解它们,然后根据结果是 true 还是 false 来选择分支。值例如

🌐 In JavaScript, constructs like if first “coerce” their conditions to booleans to make sense of them, and then choose their branches depending on whether the result is true or false. Values like

  • 0
  • NaN
  • ""(空字符串)
  • 0n(零的 bigint 版本)
  • null
  • undefined

所有值都会被强制转换为 false,其他值会被强制转换为 true。 你总是可以通过运行 Boolean 函数将值强制转换为 boolean,或者使用更简短的双重布尔否定。(后者的优点是 TypeScript 推断出一个狭窄的字面布尔类型 true,而前者推断为类型 boolean。)

🌐 all coerce to false, and other values get coerced to true. You can always coerce values to booleans by running them through the Boolean function, or by using the shorter double-Boolean negation. (The latter has the advantage that TypeScript infers a narrow literal boolean type true, while inferring the first as type boolean.)

ts
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
This kind of expression is always truthy.2872This kind of expression is always truthy.
Try

利用这种行为相当流行,尤其是在防护像 nullundefined 这样的值时。举个例子,我们尝试在 printAll 函数中使用它。

🌐 It’s fairly popular to leverage this behavior, especially for guarding against values like null or undefined. As an example, let’s try using it for our printAll function.

ts
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);
}
}
Try

你会注意到,我们通过检查 strs 是否为真值来消除了上面的错误。这至少可以防止我们在运行代码时出现令人讨厌的错误,例如:

🌐 You’ll notice that we’ve gotten rid of the error above by checking if strs is truthy. This at least prevents us from dreaded errors when we run our code like:

txt
TypeError: null is not iterable

不过请记住,对原始类型进行真值检查往往容易出错。 例如,考虑另一种编写 printAll 的尝试

🌐 Keep in mind though that truthiness checking on primitives can often be error prone. As an example, consider a different attempt at writing printAll

ts
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// DON'T DO THIS!
// KEEP READING
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
Try

我们将函数的整个主体封装在一个布尔值检查中,但这有一个微妙的缺点:我们可能不再正确处理空字符串的情况。

🌐 We wrapped the entire body of the function in a truthy check, but this has a subtle downside: we may no longer be handling the empty string case correctly.

在这里,TypeScript 对我们一点也没有伤害,但如果你对 JavaScript 不太熟悉,这种行为值得注意。 TypeScript 通常可以帮助你早期发现错误,但如果你选择对一个值什么都不做,它能做的也有限,而不会过于指示性。如果你愿意,你可以通过使用代码检查器来确保处理这种情况。

🌐 TypeScript doesn’t hurt us here at all, but this behavior is worth noting if you’re less familiar with JavaScript. TypeScript can often help you catch bugs early on, but if you choose to do nothing with a value, there’s only so much that it can do without being overly prescriptive. If you want, you can make sure you handle situations like these with a linter.

关于通过真实性进行缩小的最后一点是,带有 ! 的布尔否定会从被否定的分支中过滤出来。

🌐 One last word on narrowing by truthiness is that Boolean negations with ! filter out from negated branches.

ts
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
Try

相等性缩小

🌐 Equality narrowing

TypeScript 也使用 switch 语句和像 ===!==== 以及 != 这样的相等性检查来缩小类型范围。 例如:

🌐 TypeScript also uses switch statements and equality checks like ===, !==, ==, and != to narrow types. For example:

ts
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
(method) String.toUpperCase(): string
y.toLowerCase();
(method) String.toLowerCase(): string
} else {
console.log(x);
(parameter) x: string | number
console.log(y);
(parameter) y: string | boolean
}
}
Try

当我们检查上面的例子中 xy 都相等时,TypeScript 也知道它们的类型必须相等。由于 stringxy 都可以采用的唯一公共类型,TypeScript 知道第一分支中的 xy 必须是 string

🌐 When we checked that x and y are both equal in the above example, TypeScript knew their types also had to be equal. Since string is the only common type that both x and y could take on, TypeScript knows that x and y must be strings in the first branch.

针对特定字面值(而不是变量)进行检查也有效。我们在关于真值缩小的章节中,写了一个 printAll 函数,这个函数容易出错,因为它不小心没有正确处理空字符串。相反,我们本可以进行特定检查来排除 null,而 TypeScript 仍然会正确地从 strs 的类型中移除 null

🌐 Checking against specific literal values (as opposed to variables) works also. In our section about truthiness narrowing, we wrote a printAll function which was error-prone because it accidentally didn’t handle empty strings properly. Instead we could have done a specific check to block out nulls, and TypeScript still correctly removes null from the type of strs.

ts
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
(parameter) strs: string[]
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
(parameter) strs: string
}
}
}
Try

JavaScript 对 ==!= 的宽松相等检查也会被正确地缩小。如果你不熟悉,检查某个东西是否 == null 实际上不仅仅是检查它是否具体为值 null——它也会检查它是否可能为 undefined。同样适用于 == undefined:它会检查一个值是否为 nullundefined

🌐 JavaScript’s looser equality checks with == and != also get narrowed correctly. If you’re unfamiliar, checking whether something == null actually not only checks whether it is specifically the value null - it also checks whether it’s potentially undefined. The same applies to == undefined: it checks whether a value is either null or undefined.

ts
interface Container {
value: number | null | undefined;
}
 
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
(property) Container.value: number
 
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
Try

in 操作符缩小

🌐 The in operator narrowing

JavaScript 有一个用于判断对象或其原型链中是否具有某个属性名称的运算符:in 运算符。TypeScript 将此纳入考虑,作为缩小潜在类型的方式。

🌐 JavaScript has an operator for determining if an object or its prototype chain has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types.

例如,对于代码:"value" in x,其中 "value" 是字符串字面量,x 是联合类型。 “true” 分支会缩小 x 的类型,这些类型具有可选或必需属性 value;而 “false” 分支则缩小到具有可选或缺失属性 value 的类型。

🌐 For example, with the code: "value" in x where "value" is a string literal and x is a union type. The “true” branch narrows x’s types which have either an optional or required property value, and the “false” branch narrows to types which have an optional or missing property value.

ts
type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
 
return animal.fly();
}
Try

重申一下,可选属性将在两边都存在以进行缩小。例如,一个人可以既会游泳也会飞(使用合适的装备),因此应在 in 检查的两边都显示出来:

🌐 To reiterate, optional properties will exist in both sides for narrowing. For example, a human could both swim and fly (with the right equipment) and thus should show up in both sides of the in check:

ts
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
}
}
Try

instanceof 收窄

🌐 instanceof narrowing

JavaScript 有一个运算符用于检查一个值是否是另一个值的“实例”。 更具体地说,在 JavaScript 中,x instanceof Foo 用于检查 x原型链 是否包含 Foo.prototype。 虽然我们这里不会深入讲解,你在学习类的时候会看到更多相关内容,但它们对于大多数可以用 new 构造的值仍然很有用。 正如你可能已经猜到的,instanceof 也是一个类型保护,并且 TypeScript 会在受到 instanceof 保护的分支中进行类型缩小。

🌐 JavaScript has an operator for checking whether or not a value is an “instance” of another value. More specifically, in JavaScript x instanceof Foo checks whether the prototype chain of x contains Foo.prototype. While we won’t dive deep here, and you’ll see more of this when we get into classes, they can still be useful for most values that can be constructed with new. As you might have guessed, instanceof is also a type guard, and TypeScript narrows in branches guarded by instanceofs.

ts
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
(parameter) x: Date
} else {
console.log(x.toUpperCase());
(parameter) x: string
}
}
Try

赋值

🌐 Assignments

正如我们前面提到的,当我们为任何变量赋值时,TypeScript 会查看赋值的右侧并适当地缩小左侧。

🌐 As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately.

ts
let x = Math.random() < 0.5 ? 10 : "hello world!";
let x: string | number
x = 1;
 
console.log(x);
let x: number
x = "goodbye!";
 
console.log(x);
let x: string
Try

请注意,这些赋值都是有效的。即使在我们的第一次赋值后,x 的观察类型变为了 number,我们仍然能够将 string 赋值给 x。这是因为 x 的_声明类型_——x 最初的类型——是 string | number,而可赋值性总是针对声明类型进行检查的。

🌐 Notice that each of these assignments is valid. Even though the observed type of x changed to number after our first assignment, we were still able to assign a string to x. This is because the declared type of x - the type that x started with - is string | number, and assignability is always checked against the declared type.

如果我们将 boolean 分配给 x,我们会看到一个错误,因为那不属于声明的类型。

🌐 If we’d assigned a boolean to x, we’d have seen an error since that wasn’t part of the declared type.

ts
let x = Math.random() < 0.5 ? 10 : "hello world!";
let x: string | number
x = 1;
 
console.log(x);
let x: number
x = true;
Type 'boolean' is not assignable to type 'string | number'.2322Type 'boolean' is not assignable to type 'string | number'.
 
console.log(x);
let x: string | number
Try

控制流分析

🌐 Control flow analysis

到目前为止,我们已经通过一些基本示例了解了 TypeScript 如何在特定分支中进行类型缩小。但实际情况不仅仅是从每个变量开始,查看 ifwhile、条件语句等中的类型保护。例如

🌐 Up until this point, we’ve gone through some basic examples of how TypeScript narrows within specific branches. But there’s a bit more going on than just walking up from every variable and looking for type guards in ifs, whiles, conditionals, etc. For example

ts
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Try

padLeft 从它的第一个 if 块中返回。TypeScript 能够分析这段代码,并且在 paddingnumber 的情况下看到其余的主体 (return padding + input;) 是 不可达的。因此,它能够从 padding 的类型中移除 number(类型从 string | number 缩小到 string),用于函数的其余部分。

基于可达性的代码分析被称为_控制流分析_,TypeScript 使用这种流分析在遇到类型保护和赋值时缩小类型。 当变量被分析时,控制流可以不断分叉和重新合并,并且可以在每个点观察到该变量具有不同的类型。

🌐 This analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point.

ts
function example() {
let x: string | number | boolean;
 
x = Math.random() < 0.5;
 
console.log(x);
let x: boolean
 
if (Math.random() < 0.5) {
x = "hello";
console.log(x);
let x: string
} else {
x = 100;
console.log(x);
let x: number
}
 
return x;
let x: string | number
}
Try

使用类型谓词

🌐 Using type predicates

到目前为止,我们已经使用现有的 JavaScript 结构来处理类型缩小,但是有时你希望更直接地控制类型在整个代码中的变化方式。

🌐 We’ve worked with existing JavaScript constructs to handle narrowing so far, however sometimes you want more direct control over how types change throughout your code.

要定义一个用户自定义的类型保护,我们只需定义一个返回类型为 类型断言 的函数:

🌐 To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:

ts
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
Try

pet is Fish 在本例中是我们的类型谓词。谓词的形式为 parameterName is Type,其中 parameterName 必须是当前函数签名中某个参数的名称。

每当用某个变量调用 isFish 时,如果原始类型兼容,TypeScript 会将该变量的类型缩小到那个特定类型。

🌐 Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.

ts
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Try

请注意,TypeScript 不仅知道在 if 分支中 petFish;它还知道在 else 分支中,你没有 Fish,所以你必须有一个 Bird

🌐 Notice that TypeScript not only knows that pet is a Fish in the if branch; it also knows that in the else branch, you don’t have a Fish, so you must have a Bird.

你可以使用类型保护 isFish 来过滤 Fish | Bird 数组,并获得 Fish 数组:

🌐 You may use the type guard isFish to filter an array of Fish | Bird and obtain an array of Fish:

ts
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);
});
Try

此外,类可以 使用 this is Type 来缩小它们的类型。

🌐 In addition, classes can use this is Type to narrow their type.

断言函数

🌐 Assertion functions

类型也可以使用断言函数来缩小。

🌐 Types can also be narrowed using Assertion functions.

判别联合

🌐 Discriminated unions

到目前为止,我们看到的大多数示例都集中在用像 stringbooleannumber 这样的简单类型来缩小单个变量的范围。虽然这是很常见的,但在 JavaScript 中,大多数时候我们将处理稍微复杂一些的结构。

🌐 Most of the examples we’ve looked at so far have focused around narrowing single variables with simple types like string, boolean, and number. While this is common, most of the time in JavaScript we’ll be dealing with slightly more complex structures.

为了提供一些动机,让我们设想我们正在尝试对圆形和正方形进行编码。圆形记录它们的半径,正方形记录它们的边长。我们将使用一个叫做 kind 的字段来说明我们正在处理哪种形状。以下是定义 Shape 的第一次尝试。

🌐 For some motivation, let’s imagine we’re trying to encode shapes like circles and squares. Circles keep track of their radiuses and squares keep track of their side lengths. We’ll use a field called kind to tell which shape we’re dealing with. Here’s a first attempt at defining Shape.

ts
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
Try

注意,我们使用了字符串字面量类型的联合:"circle""square" 来告诉我们是否应将形状分别视为圆形或方形。通过使用 "circle" | "square" 而不是 string,我们可以避免拼写错误问题。

🌐 Notice we’re using a union of string literal types: "circle" and "square" to tell us whether we should treat the shape as a circle or square respectively. By using "circle" | "square" instead of string, we can avoid misspelling issues.

ts
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.2367This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.
// ...
}
}
Try

我们可以编写一个 getArea 函数,根据处理的是圆形还是正方形来应用正确的逻辑。 我们将先尝试处理圆形。

🌐 We can write a getArea function that applies the right logic based on if it’s dealing with a circle or square. We’ll first try dealing with circles.

ts
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.
}
Try

strictNullChecks 下,这会给我们一个错误——这是合适的,因为 radius 可能未定义。但如果我们对 kind 属性执行适当的检查,会怎么样呢?

🌐 Under strictNullChecks that gives us an error - which is appropriate since radius might not be defined. But what if we perform the appropriate checks on the kind property?

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.
}
}
Try

嗯,TypeScript 在这里仍然不知道该怎么办。我们已经到了这样一个地步——我们对自己的值了解得比类型检查器更多。我们可以尝试使用非空断言(在 shape.radius 之后加上 !)来表示 radius 肯定存在。

🌐 Hmm, TypeScript still doesn’t know what to do here. We’ve hit a point where we know more about our values than the type checker does. We could try to use a non-null assertion (a ! after shape.radius) to say that radius is definitely present.

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

但这感觉并不理想。 我们不得不用那些非空断言(!)向类型检查器大声‘喊话’,以说服它 shape.radius 已被定义,但这些断言在我们开始移动代码时容易出错。 此外,在 strictNullChecks 之外,我们仍然可能不小心访问这些字段(因为可选属性在读取时通常被假定总是存在)。 我们绝对可以做得更好。

🌐 But this doesn’t feel ideal. We had to shout a bit at the type-checker with those non-null assertions (!) to convince it that shape.radius was defined, but those assertions are error-prone if we start to move code around. Additionally, outside of strictNullChecks we’re able to accidentally access any of those fields anyway (since optional properties are just assumed to always be present when reading them). We can definitely do better.

这个对 Shape 的编码问题在于,类型检查器无法根据 kind 属性判断 radiussideLength 是否存在。我们需要将我们所知道的信息传达给类型检查器。考虑到这一点,让我们再尝试定义一次 Shape

🌐 The problem with this encoding of Shape is that the type-checker doesn’t have any way to know whether or not radius or sideLength are present based on the kind property. We need to communicate what we know to the type checker. With that in mind, let’s take another swing at defining Shape.

ts
interface Circle {
kind: "circle";
radius: number;
}
 
interface Square {
kind: "square";
sideLength: number;
}
 
type Shape = Circle | Square;
Try

在这里,我们已经将 Shape 正确地分为两种类型,并为 kind 属性设置了不同的值,但 radiussideLength 在各自的类型中被声明为必填属性。

🌐 Here, we’ve properly separated Shape out into two types with different values for the kind property, but radius and sideLength are declared as required properties in their respective types.

让我们看看当我们尝试访问一个 Shaperadius 时会发生什么。

🌐 Let’s see what happens here when we try to access the radius of a Shape.

ts
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
Property 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.2339Property 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.
}
Try

就像我们对 Shape 的第一个定义一样,这仍然是一个错误。 当 radius 是可选的时候,我们在启用 strictNullChecks 时得到了一个错误,因为 TypeScript 无法判断该属性是否存在。 现在 Shape 是一个联合类型,TypeScript 告诉我们 shape 可能是 Square,而 Square 上没有定义 radius! 两种解释都是正确的,但只有 Shape 的联合类型编码无论 strictNullChecks 如何配置都会导致错误。

🌐 Like with our first definition of Shape, this is still an error. When radius was optional, we got an error (with strictNullChecks enabled) because TypeScript couldn’t tell whether the property was present. Now that Shape is a union, TypeScript is telling us that shape might be a Square, and Squares don’t have radius defined on them! Both interpretations are correct, but only the union encoding of Shape will cause an error regardless of how strictNullChecks is configured.

但是如果我们再次尝试检查 kind 属性呢?

🌐 But what if we tried checking the kind property again?

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
}
}
Try

这解决了错误! 当联合类型中的每种类型都包含一个具有字面量类型的公共属性时,TypeScript 会将其视为“可区分联合类型”,并且可以缩小联合类型的成员。

🌐 That got rid of the error! When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.

在这种情况下,kind 是那个常见属性(它被认为是 Shape判别 属性)。 检查 kind 属性是否为 "circle" 消除了 Shape 中所有没有 kind 属性且类型为 "circle" 的类型。 这将 shape 缩小到类型 Circle

🌐 In this case, kind was that common property (which is what’s considered a discriminant property of Shape). Checking whether the kind property was "circle" got rid of every type in Shape that didn’t have a kind property with the type "circle". That narrowed shape down to the type Circle.

switch 语句也可以用同样的方式进行检查。现在我们可以尝试写完整的 getArea,而不需要任何讨厌的 ! 非空断言。

🌐 The same checking works with switch statements as well. Now we can try to write our complete getArea without any pesky ! non-null assertions.

ts
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
case "square":
return shape.sideLength ** 2;
(parameter) shape: Square
}
}
Try

这里重要的是对 Shape 的编码。 向 TypeScript 传达正确信息——即 CircleSquare 真的是两个有特定 kind 字段的独立类型——至关重要。 这样做让我们能够编写类型安全的 TypeScript 代码,而代码看起来与我们本来会写的 JavaScript 没有任何不同。 从那里,类型系统能够做“正确的”事情,并推断出我们 switch 语句中每个分支的类型。

🌐 The important thing here was the encoding of Shape. Communicating the right information to TypeScript - that Circle and Square were really two separate types with specific kind fields - was crucial. Doing that lets us write type-safe TypeScript code that looks no different than the JavaScript we would’ve written otherwise. From there, the type system was able to do the “right” thing and figure out the types in each branch of our switch statement.

另外,可以尝试修改上面的例子,去掉一些 return 关键字。 你会发现,类型检查有助于避免在 switch 语句中不小心落入不同分支时出现的错误。

判别联合不仅仅适用于讨论圆形和方形。它们还适合表示 JavaScript 中的任何类型的消息传递方案,例如在网络上发送消息(客户端/服务器通信)或在状态管理框架中编码变更。

🌐 Discriminated unions are useful for more than just talking about circles and squares. They’re good for representing any sort of messaging scheme in JavaScript, like when sending messages over the network (client/server communication), or encoding mutations in a state management framework.

never 类型

🌐 The never type

在缩小类型范围时,你可以将联合类型的选项减少到完全没有可能性、什么都不剩的程度。在这种情况下,TypeScript 会使用 never 类型来表示一种本不应该存在的状态。

🌐 When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.

穷举检查

🌐 Exhaustiveness checking

never 类型可以赋值给任何类型;然而,没有类型可以赋值给 never(除了 never 本身)。这意味着你可以使用类型收窄,并依赖 never 出现来在 switch 语句中进行穷尽检查。

🌐 The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.

例如,在我们的 getArea 函数中添加一个 default,尝试将形状分配给 never,在处理了所有可能的情况后不会引发错误。

🌐 For example, adding a default to our getArea function which tries to assign the shape to never will not raise an error when every possible case has been handled.

ts
type Shape = Circle | Square;
 
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;
return _exhaustiveCheck;
}
}
Try

Shape 联合类型添加新成员时,会导致 TypeScript 错误:

🌐 Adding a new member to the Shape union, will cause a TypeScript error:

ts
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 _exhaustiveCheck: never = shape;
Type 'Triangle' is not assignable to type 'never'.2322Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
Try