类型缩小

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

¥Imagine we have a function called padLeft.

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

如果 paddingnumber,它会将其视为我们想要添加到 input 的空格数。如果 paddingstring,它应该只是将 padding 前置到 input。让我们尝试实现当 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,这是正确的。换句话说,我们没有先明确检查 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

  • ""(空字符串)

    ¥"" (the empty string)

  • 0nbigint 版本零)

    ¥0n (the bigint version of zero)

  • 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 通常可以帮助你及早发现错误,但如果你选择对值不做任何事情,那么它可以做的事情就只有这么多,而不会过于规范。如果你愿意,你可以确保使用 linter 处理此类情况。

¥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:它检查一个值是 null 还是 undefined

¥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 有一个运算符用于检查一个值是否是另一个值的 “instance”。更具体地说,在 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)以用于函数的剩余部分。

¥padLeft returns from within its first if block. TypeScript was able to analyze this code and see that the rest of the body (return padding + input;) is unreachable in the case where padding is a number. As a result, it was able to remove number from the type of padding (narrowing from string | number to string) for the rest of the function.

这种基于可达性的代码分析称为控制流分析,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 必须是当前函数签名中的参数名称。

¥pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type, where parameterName must be the name of a parameter from the current function signature.

任何时候使用某个变量调用 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 不仅知道 petif 分支中的 Fish;它还知道在 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!两种解释都是正确的,但是无论 strictNullChecks 是如何配置的,只有 Shape 的联合编码会导致错误。

¥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 中没有 "circle" 类型的 kind 属性的所有类型。这将 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.

顺便说一句,尝试使用上面的示例并删除一些返回关键字。你会看到类型检查有助于避免在 switch 语句中意外遇到不同子句时出现错误。

¥As an aside, try playing around with the above example and remove some of the return keywords. You’ll see that type-checking can help avoid bugs when accidentally falling through different clauses in a switch statement.

判别联合不仅仅用于讨论圆形和正方形。它们非常适合在 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 类型可分配给每个类型;但是,没有类型可分配给 nevernever 本身除外)。这意味着你可以使用缩小范围并依靠出现的 neverswitch 语句中进行详尽检查。

¥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.

例如,将 default 添加到我们的 getArea 函数中,尝试将形状分配给 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