DZone>Web Dev Zone>TypeScript 4.4。小心谨慎
TypeScript 4.4。警惕
为了庆祝TypeScript 4.4的发布,我探讨了TypeScript中的Type Unions,以及该语言如何使用Type Guards和控制流分析来自动完善变量的类型。
-
Sep. 02, 21 - Web Dev Zone -教程
喜欢 (3)
评论
保存
鸣叫
7.59K浏览次数
加入DZone社区,获得完整的会员体验。
我将会介绍。
- 什么是类型联盟
- 类型守卫和控制流分析
- 4.4的CFA的异构条件和判别因素
简介
我曾经谈论过TypeScript的实用主义,它用类型安全来增强JavaScript的开发体验。但它不仅仅是JavaScript上面的一层薄薄的静态类型,它有一个非常强大的类型系统。在TypeScript中,可以使用类型联合、类型交叉、映射类型和条件类型从现有类型中派生出新的类型。这种类型的编程能力使我们能够减少代码的重复,并在定义类型时有一个单一的真理来源。
TypeScript
interface Employee {
name: string
age: number;
dob: Date;
}
// Type Intersection
type EmployeeWithId = Employee & { id: string };
// This type will be equivalent to
// {
// id: string;
// name: string
// age: number;
// dob: Date;
// }
// Readonly uses Mapped Types
type ReadonlyEmployee = Readonly<Employee>;
// This type will be equivalent to
// {
// readonly name: string
// readonly age: number;
// readonly dob: Date;
// }
// A Mapped Type with a Conditional Type allows for interesting transformations
type DateToString<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P];
};
type EmployeeJson = DateToString<Employee>
// This type will be equivalent to
// {
// name: string
// age: number;
// dob: string; // Date has changed to string
// }
在这篇文章中,我将深入研究Type Unions,因为TypeScript 4.4增强了编译器,支持更自然地使用union类型检查。如果你想了解以上任何一个项目的帖子,请务必喜欢并留下评论。
类型联盟很厉害
类型联盟允许我们定义一个新的类型,代表一组可能的类型中的任何一个。比如说。
TypeScript
type Primitive =
| string
| number
| boolean;
let x: Primitive;
这里,类型联盟Primitive 意味着x 可以指代string ORnumber ORboolean 的值。
TypeScript
x = 'Hello';
x = 123;
x = false;
x = new Date(); // Compiler Error
这在JavaScript世界中特别重要,因为API经常包含接受或返回不同类型值的函数。库利用动态类型来压缩许多不同的行为到一个单一的函数。通过TypeScript中的联合,而不是去最高的抽象any 或unknown ,我们可以安全地限制在一组有效的类型中。
但是,如果x 可以是这些可能的类型中的任何一个,我们可以安全地对x 。事实证明,在这种情况下,不是很好。
自动完成显示,我们能做的只有valueOf 、toString 和toLocaleString 。我们可以安全地使用联盟的唯一API是那些联盟中所有成员共有的API。
当然,我们可以对特定的类型使用一个类型断言。
TypeScript
console.log(x.toFixed(2)); // Compiler Error
const y = x as number;
console.log(y.toFixed(2)); // Valid
但是我们怎么知道我们的断言是正确的,值是正确的类型?当然,如果我们有任何疑问,在JavaScript中,我们会写一些代码来测试数据。
TypeScript
if (typeof x === 'number') {
const y = x as number;
console.log(y.toFixed(2)); // We can now safely use 'number' operations
}
TypeScript通过检查我们程序的控制流(ifs,switches等)来简化这种代码,并自动细化或缩小变量的类型(在其他语言中可能被称为智能转换)。因此,在if 块内,它自动将x 解释为number 。
TypeScript
if (typeof x === 'number') {
console.log(x.toFixed(2)); // 'x' can safely use 'number' operations
}
更重要的是,在else 块上,对于这个if ,x 的类型将是联盟的剩余部分。所以,在这种情况下,它将被缩小为string | boolean 。
编译器在这里做的事情被称为控制流分析(CFA),我们写的条件被称为类型保护。编译器可以通过if,switch,throw,return 和诸如逻辑 (&&,||)、赋值、平等和三元组 (? :) 等运算符来分析控制流。
真正的优雅之处在于,我们所写的代码与我们在JavaScript中写的一样--注意上面没有类型注释。TypeScript的不同之处在于我们得到了类型安全和更好的工具。
类型守护的类型
使用typeof ,只是一个守护的例子。让我们来分析一下类型守护的类型。我一直抵制将它们归类为类型卫士的类型,因为类型卫士太难打了。类型!
null 警卫
null 检查是最常见的卫兵之一。
TypeScript
const element: HTMLElement | null = document.getElementById('target');
element.innerText = 'Hello'; // Compiler error - null in union
if (element !== null) {
element.innerText = 'Hello'; // null removed from union
}
当然,我们可以用可选链来简化。
TypeScript
element?.innerText = 'Hello';
typeof 警卫
我们已经看到了typeof 守护的作用。下面是另一个使用开关的例子。
TypeScript
// Note that the type of 'primitive' in each branch is different
switch (typeof primitive) {
case "number": return primitive.toFixed(2);
case "string": return primitive.toUpperCase();
case "boolean": return !primitive;
default: throw new Error("Unexpected type")
}
记住,typeof 只能告诉我们一个类型是否是string,number,boolean,symbol,function,bigint,undefined 。其他的都会返回"object" 。
instanceof 守护
通过instanceof ,我们可以测试一个对象是否是一个类的实例,或者是从一个类派生出来的。
TypeScript
function setupInput(input: HTMLInputElement | HTMLTextAreaElement) {
input.value = ''; // Valid since value is common to both types
if (input instanceof HTMLTextAreaElement) {
// These properties are only valid for HTMLTextAreaElement
input.rows = 25;
input.cols = 80;
input.wrap = 'hard';
} else {
// These properties are only valid for HTMLInputElement
input.width = 400;
input.height = 50;
}
}
注意,所有在if/else块里面的属性设置在外面都会失败,因为它们对两种类型都不通用。
in 守护
in 操作符允许我们测试一个对象的成员是否存在。在下面的代码中,只有HTMLTextAreaElement 有一个rows 属性,所以通过测试它的存在,编译器确定了每个分支上的input 变量的类型,并缩小了该类型。
TypeScript
function setupInput(input: HTMLInputElement | HTMLTextAreaElement) {
if ('rows' in input) {
// These properties are only valid for HTMLTextAreaElement
input.rows = 25;
input.cols = 80;
input.wrap = 'hard';
} else {
// These properties are only valid for HTMLInputElement
input.width = 400;
input.height = 50;
}
}
注意,in 只能在联盟的所有成员都是对象类型(而不是基元)时使用。
辨别式联合的防护措施
鉴别联盟是一种设计,联盟中的类型各自包含一个成员,它可以唯一地识别或允许编译器鉴别该类型。比如说。
TypeScript
interface Dog {
kind: 'Dog';
bark(): string;
}
interface Cat {
kind: 'Cat';
purr(): string;
}
interface Fox {
kind: 'Fox';
}
type Animal =
Dog
| Cat
| Fox;
在这里,kind 属性对所有类型都是通用的,所以我们可以在一个类型为Animal 的变量上访问它。TypeScript可以使用这个属性与可能的值的比较来适当地缩小类型。
TypeScript
// animal has type Animal or 'Dog | Cat | Fox'
const animal: Animal = readAnimal();
if (animal.kind === "Fox") {
// animal has type 'Fox'
throw new Error('What does the Fox say?');
}
// 'Fox' removed because of throw above
// animal has type 'Dog | Cat'
return animal.kind === "Cat"
? animal.purr() // animal has type 'Cat'
: animal.bark(); // animal has type 'Dog'
同样,这里的好处是我们没有写任何类型注释或断言,但编译器仍然在做错误检查,IDE可以给我们很多自动完成和支持。
用户定义的卫士
在TypeScript 4.4之前,用户定义的守卫是提高此类代码可读性的唯一方法。用户定义的守卫允许我们将类型检查逻辑和守卫分解成命名函数。
例如,考虑一下前面检查HTML元素的代码。
TypeScript
if ('rows' in input) {
// input has type HTMLTextAreaElement
}
很多时候,我会将这样的检查分解到一个函数中,但编译器不会深入到函数中去寻找类型保护,所以缩小范围的工作不会发生。
TypeScript
function isTextArea(input: HTMLInputElement | HTMLTextAreaElement): boolean {
return 'rows' in input;
}
if (isTextArea(input)) {
// input still has union type
input.rows = 25; // Compiler error
}
我们必须做的是将返回类型从boolean 改为类型谓词。如果我们的布尔函数返回true ,这允许我们向编译器提供输入参数的类型信息。
TypeScript
function isTextArea(input: HTMLInputElement | HTMLTextAreaElement): input is HTMLTextAreaElement {
return 'rows' in input;
}
if (isTextArea(input)) {
// input has type HTMLTextAreaElement
input.rows = 25; // Valid
}
断言函数
我们可以以类似的方式使用断言函数。一个断言函数通知编译器,如果某些条件不正确,它将抛出一个错误。我们可以用它们来断言一个输入参数具有某种类型,然后CFA会适当地缩小范围。语法类似于用户定义的类型保护,只是我们在类型谓词前加了asserts 。
TypeScript
function assertIsTextArea(input: HTMLInputElement | HTMLTextAreaElement): asserts input is HTMLTextAreaElement {
if (!('rows' in input)) {
throw new Error("Expected a HTMLTextAreaElement");
}
}
assertIsTextArea(input);
// If we get to this line, no exception has been thrown
// Therefore, input has type HTMLTextAreaElement
input.rows = 25;
这里的优点是它去掉了缩进,但仍然缩小了。这对于开发者在函数顶部的断言(守卫的旧含义)或测试中非常有用。
TypeScript 4.4的改进之处
尽管用户定义的类型守卫和断言函数有助于提高可读性,但它们是一种粗略的工具。TypeScript 4.4编译器带来的是为变量保留控制流分析信息的能力,因此我们可以编写更自然的代码并保留缩小功能。
例如,下面的代码在TypeScript 4.4之前是行不通的,因为卫兵已经被分解成了一个变量。
TypeScript
const isTextArea = 'rows' in input;
if (isTextArea) {
// Narrowing has NOT taken place
input.rows = 25; // Compiler error
}
在TypeScript 4.4中,这段代码就可以工作了。变量isTextArea 保留了它表示input 是否是HTMLTextAreaElement 的信息,这可以被CFA拾取。它把这些称为 "别名条件"。
这些条件可以使用到目前为止所描述的所有防护措施,可以包含多个条件,甚至可以过境工作。
TypeScript
const isDog = animal.kind === 'Dog';
const isCat = animal.kind === 'Cat';
const canSpeak = isDog || isCat; // Also retains CFA information
if (!canSpeak) {
throw new Error('What does the Fox say?');
}
return isCat
? animal.purr()
: animal.bark(); // Without the throw above this would be `Fox | Dog`
canSpeak 变量是其他两个变量的 "或",但这两个变量的类型信息被合并并应用于canSpeak 。
限制
下面的限制本身并不是一种批评,而只是我对该功能的实验,以更好地理解它。
只有条件被异化
这种类型的信息异化只对条件起作用。如果我们创建变量来包含关键信息,然后再从这些变量中建立条件,那么类型保护行为就不起作用了。例如,在这里我们将typeof 结果存储在一个变量中。
TypeScript
let primitiveType = typeof primitive;
switch (primitiveType) {
case "number": return primitive.toFixed(2); // No narrowing - compiler error
case "string": return primitive.toUpperCase(); // No narrowing - compiler error
//...
}
只有顶层变量
TypeScript很聪明,正常的类型守护支持用嵌套的属性进行缩小。比如说。
TypeScript
interface InputArea {
control: HTMLTextAreaElement | HTMLInputElement;
}
interface Component {
inputArea: InputArea;
}
function setupComponent(component: Component) {
if ('rows' in component.inputArea.control) {
// These properties are only valid for HTMLTextAreaElement
component.inputArea.control.rows = 25;
component.inputArea.control.cols = 80;
component.inputArea.control.wrap = 'hard';
} else {
// These properties are only valid for HTMLInputElement
component.inputArea.control.width = 400;
component.inputArea.control.height = 50;
}
}
这里我们有一个针对嵌套在对象中的2层属性的类型防护,但编译器仍然缩小了它。然而,这在新的别名条件下不起作用(即使是单层嵌套)。它必须是一个顶层变量。
TypeScript
function setupComponent(component: Component) {
let isUsingTextArea = 'rows' in component.inputArea.control;
if (isUsingTextArea) { // Narrowing does not occur
component.inputArea.control.rows = 25; // Compiler error
// ...
递归深度限制
对于编译器应用递归规则的程度也有一个限制。我很好奇这个限制是什么,所以写了下面这段(可怕的)代码。
TypeScript
interface A {
kind: 'A';
child: Node;
}
interface B {
kind: 'B';
child: Node;
}
interface C {
kind: 'C';
child: Node;
}
interface D {
kind: 'D';
child: Node;
}
interface E {
kind: 'E';
child: Node;
}
interface F {
kind: 'F';
child: Node;
}
interface Terminal {
kind: 'Terminal';
}
type Node = A | B | C | D | E | F | Terminal;
declare const root: Node;
因此,我们有一个大型的判别联盟,其中所有成员都有一个child 的属性,除了一个,Terminal 。接下来,我创建了一系列的条件,每个条件都依赖于前面的条件。
TypeScript
const isA = root.kind === 'A';
const isAorB = isA || root.kind === "B";
const isAorBorC = isAorB || root.kind === "C";
const isAorBorCorD = isAorBorC || root.kind === "D";
const isAorBorCorDorE = isAorBorCorD || root.kind === "E";
const isAorBorCorDorEorF = isAorBorCorDorE || root.kind === "F";
我的目标是看看哪个变量我们失去了类型保护信息,以确保root 不是Terminal 的类型。我发现以下几行是有效的,因为已知root 有一个child 。
TypeScript
const childIsA1 = isAorB && root.child.kind === 'A';
const childIsA2 = isAorBorC && root.child.kind === 'A';
const childIsA3 = isAorBorCorD && root.child.kind === 'A';
const childIsA4 = isAorBorCorDorE && root.child.kind === 'A';
而下面这行则不行,因为它不知道root 是什么。
TypeScript
const childIsA5 = isAorBorCorDorEorF && root.child.kind === 'A';
// ^^^^^ - Error
表明有4层嵌套的别名。我看不出这是个问题--我只是好奇,觉得测试一下会很有趣。
总结
我希望你发现Type Guards和我一样酷。TypeScript 4.4的更新不是很华丽,但这一变化对于保持代码的清洁和安全是非常有用的,不需要添加大量的类型注释。
如果你还没有这样做,请查看我的帖子,展示TypeScript 4.1、TypeScript 4.2和TypeScript 4.3中的一些有用功能。
原文发表于此。
关于标题图片
这篇文章的标题图片是Brendan Gleeson的,取自一部名为"The Guard "的电影。这是一部关于爱尔兰警察的黑色喜剧。爱尔兰的警察服务被称为 "An Garda Síochána"(爱尔兰语中的和平卫士),其成员被称为 "卫士"。我喜欢不严谨的电影引用。
主题。
typescript, 语言, 静态类型化, 静态分析, 编译器, 工具, 新版本, 教程, 类型联盟
由Eamonn Boyle授权发表于DZone。点击这里查看原文。
DZone贡献者所表达的观点属于他们自己。
DZone上的热门文章
评论