假设我们有一个名为 padLeft
的函数。
¥Imagine we have a function called padLeft
.
tsTry
functionpadLeft (padding : number | string,input : string): string {throw newError ("Not implemented yet!");}
如果 padding
是 number
,它会将其视为我们想要添加到 input
的空格数。如果 padding
是 string
,它应该只是将 padding
前置到 input
。让我们尝试实现当 padLeft
为 padding
传递 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
.
tsTry
functionpadLeft (padding : number | string,input : string): string {return " ".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'.repeat () + padding input ;}
哦,我们在 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.
tsTry
functionpadLeft (padding : number | string,input : string): string {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
如果这看起来像是无趣的 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.
tsTry
functionpadLeft (padding : number | string,input : string): string {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
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:
tsTry
functionprintAll (strs : string | string[] | null) {if (typeofstrs === "object") {for (const'strs' is possibly 'null'.18047'strs' is possibly 'null'.s of) { strs console .log (s );}} else if (typeofstrs === "string") {console .log (strs );} else {// do nothing}}
在 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
.
tsTry
functiongetUsersOnlineMessage (numUsersOnline : number) {if (numUsersOnline ) {return `There are ${numUsersOnline } online now!`;}return "Nobody's here. :(";}
在 JavaScript 中,像 if
这样的构造首先将它们的条件 “强制转换” 到 boolean
来理解它们,然后根据结果是 true
还是 false
来选择它们的分支。像这样的值
¥In JavaScript, constructs like if
first “coerce” their conditions to boolean
s 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) -
0n
(bigint
版本零)¥
0n
(thebigint
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 boolean
s 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
.)
tsTry
// both of these result in 'true'Boolean ("hello"); // type: boolean, value: true!!This kind of expression is always truthy.2872This kind of expression is always truthy."world" ; // type: true, value: true
利用这种行为相当流行,尤其是在防范 null
或 undefined
之类的值时。例如,让我们尝试将它用于我们的 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.
tsTry
functionprintAll (strs : string | string[] | null) {if (strs && typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}
你会注意到我们已经通过检查 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
tsTry
functionprintAll (strs : string | string[] | null) {// !!!!!!!!!!!!!!!!// DON'T DO THIS!// KEEP READING// !!!!!!!!!!!!!!!!if (strs ) {if (typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}}
我们将函数的整个主体封装在真值检查中,但这有一个微妙的缺点:我们可能不再正确处理空字符串的情况。
¥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.
tsTry
functionmultiplyAll (values : number[] | undefined,factor : number): number[] | undefined {if (!values ) {returnvalues ;} else {returnvalues .map ((x ) =>x *factor );}}
相等性缩小
¥Equality narrowing
TypeScript 还使用 switch
语句和 ===
、!==
、==
和 !=
等相等性检查来缩小类型。例如:
¥TypeScript also uses switch
statements and equality checks like ===
, !==
, ==
, and !=
to narrow types.
For example:
tsTry
functionexample (x : string | number,y : string | boolean) {if (x ===y ) {// We can now call any 'string' method on 'x' or 'y'.x .toUpperCase ();y .toLowerCase ();} else {console .log (x );console .log (y );}}
当我们在上面的示例中检查 x
和 y
是否相等时,TypeScript 知道它们的类型也必须相等。由于 string
是 x
和 y
都可以采用的唯一通用类型,因此 TypeScript 知道第一个分支中的 x
和 y
必须是 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 string
s 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 null
s, and TypeScript still correctly removes null
from the type of strs
.
tsTry
functionprintAll (strs : string | string[] | null) {if (strs !== null) {if (typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}}
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
.
tsTry
interfaceContainer {value : number | null | undefined;}functionmultiplyValue (container :Container ,factor : number) {// Remove both 'null' and 'undefined' from the type.if (container .value != null) {console .log (container .value );// Now we can safely multiply 'container.value'.container .value *=factor ;}}
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
.
tsTry
typeFish = {swim : () => void };typeBird = {fly : () => void };functionmove (animal :Fish |Bird ) {if ("swim" inanimal ) {returnanimal .swim ();}returnanimal .fly ();}
重申一下,可选属性将存在于两侧以进行缩小。例如,一个人可以游泳和飞行(使用合适的设备),因此应该出现在 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:
tsTry
typeFish = {swim : () => void };typeBird = {fly : () => void };typeHuman = {swim ?: () => void;fly ?: () => void };functionmove (animal :Fish |Bird |Human ) {if ("swim" inanimal ) {animal ;} else {animal ;}}
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 instanceof
s.
tsTry
functionlogValue (x :Date | string) {if (x instanceofDate ) {console .log (x .toUTCString ());} else {console .log (x .toUpperCase ());}}
赋值
¥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.
tsTry
letx =Math .random () < 0.5 ? 10 : "hello world!";x = 1;console .log (x );x = "goodbye!";console .log (x );
请注意,这些分配中的每一个都是有效的。即使在我们第一次分配后观察到的 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.
tsTry
letx =Math .random () < 0.5 ? 10 : "hello world!";x = 1;console .log (x );Type 'boolean' is not assignable to type 'string | number'.2322Type 'boolean' is not assignable to type 'string | number'.= true; x console .log (x );
控制流分析
¥Control flow analysis
到目前为止,我们已经通过一些基本示例来了解 TypeScript 如何在特定分支中缩小类型。但是,除了从每个变量中查找 if
、while
、条件等中的类型保护之外,还有更多的事情要做。例如
¥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 if
s, while
s, conditionals, etc.
For example
tsTry
functionpadLeft (padding : number | string,input : string) {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
padLeft
从其第一个 if
块内返回。TypeScript 能够分析此代码并发现在 padding
是 number
的情况下,主体的剩余部分 (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.
tsTry
functionexample () {letx : string | number | boolean;x =Math .random () < 0.5;console .log (x );if (Math .random () < 0.5) {x = "hello";console .log (x );} else {x = 100;console .log (x );}returnx ;}
使用类型谓词
¥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:
tsTry
functionisFish (pet :Fish |Bird ):pet isFish {return (pet asFish ).swim !==undefined ;}
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.
tsTry
// Both calls to 'swim' and 'fly' are now okay.letpet =getSmallPet ();if (isFish (pet )) {pet .swim ();} else {pet .fly ();}
请注意,TypeScript 不仅知道 pet
是 if
分支中的 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
:
tsTry
constzoo : (Fish |Bird )[] = [getSmallPet (),getSmallPet (),getSmallPet ()];constunderWater1 :Fish [] =zoo .filter (isFish );// or, equivalentlyconstunderWater2 :Fish [] =zoo .filter (isFish ) asFish [];// The predicate may need repeating for more complex examplesconstunderWater3 :Fish [] =zoo .filter ((pet ):pet isFish => {if (pet .name === "sharkey") return false;returnisFish (pet );});
另外,类可以 使用 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
到目前为止,我们看到的大多数示例都集中在使用简单类型(如 string
、boolean
和 number
)来缩小单个变量的作用域。虽然这很常见,但大多数时候在 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
.
tsTry
interfaceShape {kind : "circle" | "square";radius ?: number;sideLength ?: number;}
请注意,我们使用的是字符串字面类型的联合:"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.
tsTry
functionhandleShape (shape :Shape ) {// oops!if (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.shape .kind === "rect") {// ...}}
我们可以编写一个 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.
tsTry
functiongetArea (shape :Shape ) {return'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.Math .PI *shape .radius ** 2;}
在 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?
tsTry
functiongetArea (shape :Shape ) {if (shape .kind === "circle") {return'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.Math .PI *shape .radius ** 2;}}
嗯,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.
tsTry
functiongetArea (shape :Shape ) {if (shape .kind === "circle") {returnMath .PI *shape .radius ! ** 2;}}
但这感觉并不理想。我们不得不用那些非空断言(!
)对类型检查器大喊大叫,以说服它定义了 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
属性知道是否存在 radius
或 sideLength
。我们需要将我们所知道的信息传达给类型检查器。考虑到这一点,让我们再次定义 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
.
tsTry
interfaceCircle {kind : "circle";radius : number;}interfaceSquare {kind : "square";sideLength : number;}typeShape =Circle |Square ;
在这里,我们已经正确地将 Shape
分成了 kind
属性具有不同值的两种类型,但是 radius
和 sideLength
在它们各自的类型中被声明为必需的属性。
¥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.
让我们看看当我们尝试访问 Shape
的 radius
时会发生什么。
¥Let’s see what happens here when we try to access the radius
of a Shape
.
tsTry
functiongetArea (shape :Shape ) {returnProperty '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'.Math .PI *shape .** 2; radius }
就像我们对 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 Square
s 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?
tsTry
functiongetArea (shape :Shape ) {if (shape .kind === "circle") {returnMath .PI *shape .radius ** 2;}}
这摆脱了错误!当联合中的每个类型都包含具有字面类型的公共属性时,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.
tsTry
functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;}}
这里重要的是 Shape
的编码。向 TypeScript 传达正确的信息 - Circle
和 Square
实际上是两种不同的类型,具有特定的 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
类型可分配给每个类型;但是,没有类型可分配给 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.
例如,将 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.
tsTry
typeShape =Circle |Square ;functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;default:const_exhaustiveCheck : never =shape ;return_exhaustiveCheck ;}}
向 Shape
联合添加新成员,将导致 TypeScript 错误:
¥Adding a new member to the Shape
union, will cause a TypeScript error:
tsTry
interfaceTriangle {kind : "triangle";sideLength : number;}typeShape =Circle |Square |Triangle ;functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;default:constType 'Triangle' is not assignable to type 'never'.2322Type 'Triangle' is not assignable to type 'never'.: never = _exhaustiveCheck shape ;return_exhaustiveCheck ;}}