- 阐明:目前网上没有 TypeScript 最新官网文档的中文翻译,所以有了这么一个翻译打算。因为我也是 TypeScript 的初学者,所以无奈保障翻译百分之百精确,若有谬误,欢送评论区指出;
- 翻译内容:暂定翻译内容为 TypeScript Handbook,后续有空会补充翻译文档的其它局部;
- 我的项目地址:TypeScript-Doc-Zh,如果对你有帮忙,能够点一个 star ~
本章节官网文档地址:Narrowing
类型膨胀
假如当初有一个叫做 padLeft
的函数:
function padLeft(padding: number | string, input: string): string { trjow new Error('Not implemented yet!')}
如果 padding
是 number
类型,那么它将作为 input
前缀空格的个数,如果它是 string
类型,那么它将间接作为 input
的前缀。当初咱们尝试实现一下相干的逻辑,假设要给 padLeft
传入 number
类型的 padding
参数。
function padLeft(padding: number | string, input: 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'.}
啊这,传入 padding
参数的时候报错了。TypeScript 正告咱们,将 number
增加给 number | string
可能会失去冀望之外的后果,事实上也的确如此。换句话说,咱们没有在一开始显式查看 padding
是否是一个 number
,同时咱们也没有解决它是 string
的状况。所以咱们来改良一下代码吧。
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return " ".repeat(padding) + input; } return padding + input;}
如果你感觉这看起来和无趣的 JavaScript 代码一样,那你可说到点上了。除了咱们增加的类型注解之外,这些 TypeScript 代码看起来的确很像 JavaScript。这里的重点在于, TypeScript 的类型零碎旨在让开发者尽可能轻松地编写惯例的 JavaScript 代码,而不用为了取得类型平安而费尽心思。
尽管看起来可能不多,但实际上这个过程藏着很多机密。就像 TypeScript 如何应用动态类型剖析运行时的值一样,它将类型剖析笼罩在相似于 if/else
这样的 JavaScript 运行时控制流构造上,同时还包含了三元表达式、循环、真值查看等,这些都能对类型产生影响。
在 if
条件查看语句中,TypeScript 发现了 typeof padding === "number"
,并将其视为一种称之为“类型爱护”的非凡代码构造。TypeScript 遵循咱们的程序可能达到的执行门路,并在给定的地位剖析某个值可能取到的最具体类型。它会查看这些非凡的查看语句(也就是“类型爱护”)和赋值语句,并将申明的类型精炼为更具体的类型,这就是所谓的“类型膨胀”。在很多编辑器中,咱们能够察看到这些类型的变动。
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return " ".repeat(padding) + input; ^^^^^ // (parameter) padding: number } return padding + input; ^^^^^^^ // (parameter) padding: string}
TypeScript 能够了解几种不同的用于膨胀类型的结构。
typeof
类型爱护
正如咱们所看到的,JavaScript 反对的 typeof
运算符能够给出对于运行时值的类型的根本信息。同样的,TypeScript 冀望该运算符能够返回如下确定的字符串:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
就像咱们在 padLeft
中看到的,这个运算符经常出现在大量的 JavaScript 库中,而 TypeScript 也能了解这个运算符,从而在不同的分支中膨胀类型。
在 TypeScript 中,查看 typeof
的返回值就是一种类型爱护的形式。因为 TypeScript 能够编码 typeof
对不同值的操作形式,所以它也晓得这个运算符在 JavaScript 中的一些怪异体现。举个例子,留神看下面的列表,typeof
没有返回字符串 "null"
。再看上面的例子:
function printAll(strs: string | string[] | null) { if (typeof strs === "object") { for (const s of strs) { ^^^^ // Object is possibly 'null'. console.log(s); } } else if (typeof strs === "string") { console.log(strs); } else { // do nothing }}
在 printAll
函数中,咱们试图查看 strs
是否是一个对象,从而判断它是不是数组类型(在 JavaScript 中,数组也属于对象类型)。但在 JavaScript 中,typeof null
实际上会返回 "object"
!这是历史遗留 bug 中的其中一个。
有短缺教训的开发者可能不会感到很诧异,但并不是每一个人都曾在 JavaScript 中遇到这个问题。侥幸的是,TypeScript 让咱们晓得 strs
只是膨胀到 string[] | null
类型而不是 string[]
类型。
这可能是解说“真值”查看的一个不错的引子。
真值膨胀
Truthiness 这个词可能在词典中找不到,但你肯定在 JavaScript 中听过这个货色。
在 JavaScript 中,咱们能够在条件语句中应用任意的表达式,比方 &&
、||
、if
语句、布尔值取反(!
)等。举个例子,if
语句并没有要求它的条件肯定是 boolean
类型。
function getUsersOnlineMessage(numUsersOnline: number) { if (numUsersOnline) { return `There are ${numUsersOnline} online now!`; } return "Nobody's here. :(";}
在 JavaScript 中,相似 if
这样的构造会首先将条件“强制转化为”一个 boolean
类型的值,从而确保承受的参数是正当的,之后基于后果是 true
还是 false
,会抉择对应的分支。
相似上面这样的值通过转化后都会成为 false
:
0
NaN
""
(空字符串)0n
(0 的bigint
版本)null
undefined
除此之外的其余值通过转化后都会成为 true
。你总能够通过调用 Boolean
函数将值转化为 boolean
类型,或者应用更加简短的 !!
。(后者的劣势在于,TypeScript 能够将其推断为一个更具体的字面量布尔值类型 true
,而前者只能被推断为 boolean
)
// 上面的后果都是 trueBoolean("hello"); // type: boolean, value: true!!"world"; // type: true, value: true
在编码中常常会用到这个个性,尤其多用于防止出现像 null
或者 undefined
这样的值。举个例子,咱们尝试在 printAll
函数中应用:
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); }}
能够看到,通过查看 strs
是否是真值,咱们胜利解脱了之前呈现的报错。这至多能够防止出现像上面这样令人胆怯的谬误:
TypeError: null is not iterable
然而请记住,对原始类型的真值查看经常容易出错。举个例子,咱们尝试像上面这样改写 printAll
:
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); } }}
咱们将整个函数体包裹在一个真值查看中,但这样做其实有一个潜在的问题:咱们可能再也无奈正确地解决空字符串的状况。
TypeScript 在这里并不会给出报错提醒,但如果你不相熟 JavaScript 的话,这是一个值得关注的事件。TypeScript 总是可能帮忙你提前捕捉 bug,但如果你抉择对某个值不做任何解决,那么在确保不适度束缚的前提下,TypeScript 能做的也就只有这么多了。如果你需要的话,能够用一个 linter 确保本人正确处理了相似这样的状况。
对于真值膨胀,最初一点要阐明的是,布尔值取反 !
能够筛选出否定分支:
function multiplyAll( values: number[] | undefined, factor: number): number[] | undefined { if (!values) { return values; } else { return values.map((x) => x * factor); }}
相等性膨胀
TypeScript 也应用 switch
语句和诸如 ===
、!==
、==
和 !=
这样的相等性查看来膨胀类型。举个例子:
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 }}
在上述例子中,当咱们通过查看得悉 x
和 y
是相等的时候,TypeScript 晓得它们的类型也必须是相等的。因为 string
是 x
和 y
共有的类型,所以 TypeScript 晓得 x
和 y
在第一个逻辑分支中必定都是 string
类型。
同样的,咱们也能够查看特定的字面量值(和变量绝对)。在后面解说真值膨胀的例子中,咱们编写的 printAll
函数存在潜在的谬误,因为它没有适当地解决空字符串的状况。无妨换一种思路,咱们通过一个特定的查看排除 null
,这样 TypeScript 也依然能够将 null
从 strs
的类型中正确地移除。
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 } }}
JavaScript 的涣散相等性查看 ==
和 !=
同样也能够正确地膨胀类型。可能你还不太熟悉,查看某个值是否== null
的时候,不仅仅是在查看这个值是否确切地等于 null
,也是在查看这个值是否是潜在的 undefined
。对于 == undefined
也同理:它会查看这个值是否等于 null
或者 undefined
。
interface Container { value: number | null | undefined;} function multiplyValue(container: Container, factor: number) { // 这个查看能够同时移除 null 和 undefined if (container.value != null) { console.log(container.value); ^^^^^^ // (property) Container.value: number // 当初能够平安地进行计算 container.value *= factor; }}
in
操作符膨胀
JavaScript 的 in
操作符能够判断对象是否有某个属性。TypeScript 将其视为一种膨胀潜在类型的形式。
举个例子,假设有代码 "value" in x
,"value"
是一个字符串字面量,x
是一个联结类型。那么后果为 true
的分支会将 x
膨胀为具备可选属性或必须属性 value
的类型,而后果为 false
的分支则会将 x
膨胀为具备可选属性或缺失属性 value
的类型。
type Fish = { swim: () => void };type Bird = { fly: () => void }; function move(animal: Fish | Bird) { if ("swim" in animal) { return animal.swim(); } return animal.fly();}
再次重申,可选的属性在膨胀时会同时呈现在两个分支中。举个例子,人类既能游泳也能飞(我指的是通过交通工具),因而在 in
查看中,这个类型会同时呈现在两个分支中:
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 }}
instanceof
膨胀
JavaScript 有一个操作符能够查看某个值是否是另一个值的实例。更具体地说,在 JavaScript 中,x instanceof Foo
能够查看 x
的原型链上是否蕴含 Foo.prototype
。尽管咱们在这里不会深入探讨,而且后续解说类的时候会波及更多这方面的内容,但它对大多数能够由 new
结构的值来说依然很有用。你可能曾经猜到了,instanceof
也是类型爱护的一种形式,TypeScript 能够在由 instanceof
爱护的分支里膨胀类型。
function logValue(x: Date | string) { if (x instanceof Date) { console.log(x.toUTCString()); ^ // (parameter) x: Date } else { console.log(x.toUpperCase()); ^ // (parameter) x: string }}
赋值
正如咱们先前提到的,当咱们给任意变量赋值的时候,TypeScript 会查看赋值语句右部,对左部的变量类型进行适合的膨胀。
let x = Math.random() < 0.5 ? 10 : "hello world!"; ^ // let x: string | numberx = 1; console.log(x); ^ // let x: numberx = "goodbye!"; console.log(x); ^ // let x: string
留神这些赋值语句都是无效的。尽管在第一次赋值之后,x
的可察看类型变成了 number
,但咱们依然能够给它赋值 string
类型的值。这是因为 x
的申明类型 —— 也就是 x
的初始类型,是 string | number
,而可赋值性总是会基于申明类型进行查看。
如果咱们赋值给 x
一个 boolean
类型的值,那么就会抛出一个谬误,因为在申明类型中并不存在 boolean
类型。
let x = Math.random() < 0.5 ? 10 : "hello world!"; ^ // let x: string | numberx = 1; console.log(x); ^ // let x: numberx = true;^// Type 'boolean' is not assignable to type 'string | number'. console.log(x); ^ // let x: string | number
控制流剖析
到目前为止,咱们曾经通过一些根本的例子解说了 TypeScript 是如何在具体的分支中膨胀类型的。但除了剖析每个变量,在 if
、while
等条件语句中查找类型爱护之外,TypeScript 还做了不少其余工作。
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return " ".repeat(padding) + input; } return padding + input;}
padLeft
在第一个 if
块中返回。TypeScript 能够对这段代码进行剖析,并发现函数体的残余局部(return padding + input;
)在 padding
为 number
的时候是不可达的。最初,针对函数体的残余局部,它能够将 number
从 padding
的类型中移除(也就是将类型 string | number
膨胀为 string
)。
这种基于可达性的代码剖析称为“控制流剖析”。在遇到类型爱护和赋值语句的时候,TypeScript 会应用这种流剖析去膨胀类型。当剖析一个变量的时候,控制流能够一直被拆开与从新合并,而咱们也能够察看到变量在每个节点有不同的类型。
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}
应用类型谓词
目前为止,咱们都是应用现成的 JavaScript 构造去解决类型膨胀,但有时候,你可能想要更加间接地去管制类型在代码中的变动。
要实现一个用户自定义的类型爱护,咱们只须要定义一个返回类型谓词的函数即可:
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined;}
在本例中,pet is Fish
就是一个类型谓词。类型谓词的模式是 paramenterName is Type
,parameterName
必须是以后函数签名的参数名。
任何时候,只有给 isFish
传递参数并调用它,TypeScript 就会在该类型兼容初始类型的时候,将变量类型膨胀为该具体的类型。
// 对 swim 和 fly 的调用都是能够的let pet = getSmallPet(); if (isFish(pet)) { pet.swim();} else { pet.fly();}
留神,TypeScript 不仅晓得在 if
分支中 pet
是 Fish
,也晓得在 else
分支中其对应的类型,因为不是 Fish
那就必定是 Bird
了。
你也能够应用 isFish
这个类型爱护从一个 Fish | Bird
类型的数组中筛选出一个仅蕴含 Fish
类型的数组:
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];const underWater1: Fish[] = zoo.filter(isFish);// 或者应用const underWater2: Fish[] = zoo.filter(isFish) as Fish[]; // 在更简单的例子中,可能须要反复类型谓词const underWater3: Fish[] = zoo.filter((pet): pet is Fish => { if (pet.name === "sharkey") return false; return isFish(pet);});
此外,类能够应用 this is Type 去膨胀类型。
可辨识的联结类型
目前为止,咱们看到的大多数例子都是将单个变量膨胀为简略类型,诸如 string
、boolean
和 number
等。尽管这很常见,但在 JavaScript 中,咱们很多时候须要解决略微简单一些的构造。
假如咱们当初须要编码表示圆形和正方形的形态,圆形须要用到半径,正方形须要用到边长。咱们会应用 kind
域表明以后正在解决的形态。以下是第一种定义 Shape
的形式:
interface Shape { kind: "circle" | "square"; radius?: number; sideLength?: number;}
留神咱们这里应用了字符串字面量类型的联结: "circle"
和 "square"
。它能够通知咱们以后正在解决的形态是圆形还是正方形。通过应用 "circle" | "square"
而不是 string
,咱们能够防止拼写错误。
function handleShape(shape: Shape) { // oops! if (shape.kind === "rect") {// 该条件始终返回 false,因为类型 "circle" | "square" 和类型 "rect" 不存在重叠。 // ... }}
咱们能够编写一个 getArea
函数,它能够基于以后解决的形态的类型应用对应的逻辑。首先咱们来解决一下圆形:
function getArea(shape: Shape) { return Math.PI * shape.radius ** 2;// 对象可能是 'undefined'}
在启用 strictNullChecks 的状况下会抛出一个谬误 —— 这是正当的,毕竟 radius
可能没有定义。但如果咱们对 kind
属性进行正当的查看呢?
function getArea(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius ** 2; // Object might be 'undefined' }}
emm,TypeScript 依然无从下手。咱们这里刚好遇到了一个场景,那就是咱们把握的对于这个值的信息比类型查看器要多。因而,这里能够应用一个非空值断言(给 shape.radius
增加后缀 !
)表明 radius
肯定是存在的。
function getArea(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius! ** 2; }}
但这种解决形式仿佛不是很现实。咱们不得不给类型查看器增加大量的非空值断言(!
),让它确信 shape.radius
曾经被定义好了,但如果把代码移除,这些断言就很容易造成谬误。此外,在禁用 strictNullChecks 的状况下,咱们可能会意外地拜访到其它域(毕竟读取可选属性的时候,TypeScript 会假设它们是存在的)。总而言之,该当有更好的解决形式。
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
在对应的类型中成为了必须属性。
咱们来看下试图拜访 Shape
的 radius
会产生什么事:
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'.}
就像之前第一次定义 Shape
的时候一样,依然抛出了一个谬误。之前,当 radius
是可选属性的时候,咱们看到了一个报错(仅在启用 strictNullChecks 的状况下),因为 TypeScript 无从得悉这个属性是否真的存在。而当初 Shape
曾经是一个联结类型了,TypeScript 通知咱们 shape
可能是 Square
,而 Square
是没有定义 radius
属性的!两种解释都是正当的,但只有后者会在禁用 strictNullChecks 的状况下依然抛出一个谬误。
那么,如果这时候咱们再次查看 kind
属性会怎么样呢?
function getArea(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius ** 2; ^^^^^ // (parameter) shape: Circle }}
代码不再报错了!当联结类型中的每个类型都蕴含一个字面量类型的公共属性的时候,TypeScript 会将其视为一个可辨识的联结类型,并通过膨胀确认类型为联结类型的某个成员。
在本例中,kind
就是那个公共属性(也就是 Shape
的一个可辨识属性)。通过查看 kind
属性是否为 "circle"
,咱们能够排除掉 Shape
中所有 kind
属性值不为 "circle"
的类型。也就是说,能够将 shape
类型膨胀为 Circle
类型。
同理,这种查看也能够用于 switch
语句中。当初咱们能够编写一个残缺的 getArea
函数了,而且它没有任何麻烦的 !
非空值断言符号。
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 }}
这个例子的重点在于 Shape
的编码。将重要的信息传播给 TypeScript 十分重要,咱们得通知它,Circle
和 Square
是两种不同的类型,有各自的 kind
属性值。这样咱们就能够编写类型平安的 TypeScript 代码,它看起来与咱们编写的 JavaScript 没有什么不同。晓得了这一点之后,类型零碎也能够做“正确的”解决,在 switch
的每个分支中弄清具体的类型。
顺便一提,你能够尝试编写下面的示例并删除一些返回关键字。你将看到,在 switch 语句中意外遇到不同子句时,类型查看能够无效防止 bug 的呈现
可辨识联结类型的用途十分大,不仅仅是用在本例的圆形和正方形中。它们还实用于示意 JavaScript 中任意类型的消息传递计划,比方在网络上发送音讯(客户端/服务端通信)或在状态治理框架中的 mutation 进行编码等。
never
类型
在膨胀类型的时候,你能够将联结类型缩小到一个仅存的类型,这时候,你基本上曾经排除了所有的可能性,并且没有残余的类型可选了。此时,TypeScript 会应用 never
类型去示意一个不应该存在的状态。
穷举查看
never
类型能够赋值给任意一个类型,然而,除了 never
自身,没有任意一个类型能够赋值给 never
。这意味着你能够应用类型膨胀和 never
在一个 swicth
语句块中进行穷举查看。
举个例子,在 getArea
函数的 default
分支中,咱们能够把 shape
赋值给 never
类型的值。这样,当任意一个可能的状况没有在后面的分支失去解决的时候,在这个分支中就必然会抛出谬误。
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; }}
给 Shape
联结类型增加一个新成员,将会导致 TypeScript 抛出一个谬误:
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'. return _exhaustiveCheck; }}