阅读 917

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

作者:Marius Schulz

译者:前端小智

来源:Marius Schulz


基于控制流的类型分析

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 类型来简化可空变量的处理。最后,控制流分析防止引用在给定位置没有明确分配的变量。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:mariusschulz.com/blog/contro…

原文:mariusschulz.com/blog/read-o…

交流(欢迎加入群,群工作日都会发红包,互动讨论技术)

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

关注下面的标签,发现更多相似文章
评论