【TypeScript 演化史 -- 第二章】基于控制流的类型分析 和 只读属性

基于控制流的类型分析

TypeScript 官网总结了基于控制流的类型分析:

TypeScript 2.0 实现了对局部变量和参数的控制流类型分析。以前,对类型保护进行类型分析仅限于 if 语句和 ?: 条件表达式,并且不包括赋值和控制流结构的影响,例如 returnbreak 语句。
使用 TypeScript 2.0,类型检查器会分析语句和表达式所有可能的控制流,在任何指定的位置对声明为联合类型的局部变量或参数产生最可能的具体类型(缩小范围的类型)。

这是一个很深奥的解释。下面的示例演示了 TypeScript 如何理解赋值给局部变量的影响,以及如何相应地缩小该变量的类型:

let command: string | string[]; command = "pwd"; command.toLowerCase(); // 这里,command 的类型是 'string' command = ["ls", "-la"]; command.join(" "); // 这里,command 的类型是 'string[]'

注意,所有代码都位于同一个作用域内。尽管如此,类型检查器在任何给定位置都为 command 变量使用最具体的类型

  • 在分配了字符串 “pwd” 之后,command 变量就不可能是字符串数组(联合类型中惟一的其他选项)。因此,TypeScript 将 command 作为 string 类型的变量,并允许调用toLowerCase() 方法。
  • 在分配了字符串数组 ["ls", "-la"] 之后,command 变量不再被视为字符串,现在它是一个字符串数组,所以对 join 方法的也就能调用了。

同样由于进行了相同的控制流分析,因此以下函数在 TypeScript 2.0 也可以正确进行了类型检查:

function composeCommand(command: string | string[]): string{ if (typeof command === 'string') { return command; } return command.join(' ') }

编译器现在知道,如果 command 参数的类型是 string,那么函数总是在 if 语句中提前返回。由于提前的退出行为,command 参数的类型在 if 语句之后被限制为string[]。因此,对 join 方法的调用将正确地检查类型。

TypeScript 2.0 之前,编译器无法推断出上面的语义。因此,没有从 command 变量的联合类型中删除字符串类型,并产生以下编译时错误:

Property 'join' does not exist on type 'string | string[]'.

严格的 Null 检查

当与可空类型一起使用时,基于控制流的类型分析尤其有用,可空类型使用包括 nullundefined 在联合类型中的表示。通常,在使用可空类型的变量之前,我们需要检查该变量是否具有非空值:

type Person = { firstName: string; lastName?: string | null | undefined; }; function getFullName(person: Person): string { const { firstName, lastName } = person; // 在这里,我们检查 `lastName` 属性的 虚值(falsy), // 包含 `null` 和 `undefined`(以及其它值,例如 `""`) //包含`null`和`undefined`(以及其他值,例如“”) if (!lastName) { return firstName; } return `${firstName} ${lastName}`; }

在此,Person 类型定义了一个不可为空的 firstName 属性和一个可为空的 lastName 属性。 如果我们要返回全名,则需要检查 lastNamenull 或者undefined ,以避免将字符串 "null""undefined" 附加到名字上。

为了清晰可见,我将 undefined 的类型添加到 lastName 属性的联合类型中,尽管这是多余的做法。 在严格的 null 检查模式下,undefined 的类型会自动添加到可选属性的联合类型中,因此我们不必显式将其写出。

明确赋值分析

基于控制流的另一个新特性是明确赋值分析。在严格的 null 检查模式下,对类型不允许为 undefined 的局部变量有明确赋值的分析:

let name: string; // Error: 在赋值前使用了变量 “name” console.log(name);

该规则的一个例外是类型包括 undefined 的局部变量

let name: string | undefined; console.log(name); // No error

明确的赋值分析是另一种针对可空性缺陷的保护措施。其思想是确保每个不可空的局部变量在使用之前都已正确初始化。

只读属性

TypeScript 2.0 中,readonly 修饰符被添加到语言中。使用 readonly 标记的属性只能在初始化期间或从同一个类的构造函数中分配,其他情况一律不允许。

来看一个例子。下面是一个简单的 Point 类型,它声明了两个只读属性 xy

type Point = { readonly x: number; readonly y: number; };

现在,我们可以创建一个表示原点 point(0, 0) 的对象:

const origin: Point = { x:0, y:0 };

由于 xy 标记为 readonly,因此我们无法更改这两个属性的值:

// 错误:赋值表达式的左侧 // 不能是常量或只读属性 origin.x = 100;

一个更现实的例子

虽然上面的示例可能看起来有些做作(确实是这样),但是请考虑下面这样的函数:

function moveX(p: Point, offset: number): Point { p.x += offset; return p; }

moveX 函数不能修改给定 px 属性。因为 x 是只读的,如果尝试这么,TypeScript 编译器会给出错误提示:

相反,moveX 应该返回一个具有更新的属性值的 point,它类似这样的:

function moveX(p: Point, offset: number): Point { return { x: p.x + offset, y: p.y }; }

只读类属性

咱们还可以将 readonly 修饰符应用于类中声明的属性。如下所示,有一个 Circle 类,它有一个只读 的radius 属性和一个get area 属性,后者是隐式只读的,因为没有 setter:

class Circle { readonly radius: number; constructor(radius: number) { this.radius = radius; } get area() { return Math.PI * this.radius ** 2; } }

注意,使用 ES7 指数运算符对 radius 进行平方。radiusarea 属性都可以从类外部读取(因为它们都不是私有(private)的),但是不能写入(因为它们都是只读(readonly)的):

const unitCircle = new Circle(1); unitCircle.radius; // 1 unitCircle.area; // 3.141592653589793 // 错误:赋值表达式的左侧 // 不能是常量或只读属性 unitCircle.radius = 42; // Error: Left-hand side of assignment expression // cannot be a constant or read-only property unitCircle.area = 42;

只读索引签名

此外,可以使用 readonly 修饰符标记索引签名。ReadonlyArray<T> 类型使用这样的索引签名来阻止对索引属性的赋值:

interface ReadonlyArray<T> { readonly length: number; // ... readonly [n: number]: T; }

由于只读索引签名,编译器将以下赋值标记为无效

const primesBelow10: ReadonlyArray<number> = [2, 3, 5, 7]; // Error: 类型 “ReadonlyArray<number>” 中的索引签名仅允许读取 primesBelow10[4] = 11;

只读与不变性

readonly 修饰符是TypeScript类型系统的一部分。它只被编译器用来检查非法的属性分配。一旦TypeScript代码被编译成JavaScript,所有readonly的概念都消失了。您可以随意摆弄这个小示例,看看如何转换只读属性。

因为 readonly 只是一个编译时工件,所以没有针对运行时的属性分配的保护。也就是说,它是类型系统的另一个特性,通过让编译器从 TypeScript 代码库中检查意外的属性分配,帮助你编写正确的代码。

总结

基于控制流的类型分析是 TypeScript 类型系统的一个强大的补充。类型检查器现在理解了控制流中赋值和跳转的语义,从而大大减少了对类型保护的需要。可以通过消除 nullundefined 类型来简化可空变量的处理。最后,控制流分析防止引用在给定位置没有明确分配的变量。

原文:https://mariusschulz.com/blog/control-flow-based-type-analysis-in-typescript