[译]TypeScript 4.4 Beta版本发布

2,887 阅读12分钟

原文:devblogs.microsoft.com/typescript/…

TypeScript4.4版本的一些主要亮点:

  • 别名条件下的控制流分析
  • Symbol和模板字符串模式索引签名
  • Catch变量中默认为unknown类型(--useUnknownInCatchVariables)
  • 严格的可选属性类型(--exactOptionalPropertyTypes)
  • tsc --help的更新和提升
  • 性能提升
  • 对于JavaScript的拼写建议
  • 内嵌提示
  • 不兼容的变更

别名条件下的控制流分析

在JavaScript中,我们经常需要用不同的方式去探测一个变量,看看它是否有一个我们可以使用的更加具体的类型。TypeScript理解这些探测并且把它们称作类型保护(type guards)。当我们使用变量类型时,类型检查器不必让TypeScript相信它的类型,而是利用一种称为控制流分析(control flow analysis)的方法来推断每个语言构造中的类型。

例如,我们可以这么写:

function foo(arg: unknown) {
    if (typeof arg === "string") {
        // We know this is a string now.
        console.log(arg.toUpperCase());
    }
}

在这个例子中,我们检查arg是否是一个string。TypeScript识别了typeof arg===“string”检查,它认为这是一个类型保护,并且能够确定arg应该是if块主体中的字符串。

然而,如果我们将条件移出到一个常量中呢?

function foo(arg: unknown) {
    const argIsString = typeof arg === "string";
    if (argIsString) {
        console.log(arg.toUpperCase());
        //              ~~~~~~~~~~~
        // Error! Property 'toUpperCase' does not exist on type 'unknown'.
    }
}

在之前版本的TypeScript中,这里会报错——即使argIsString被分配了一个类型守卫的值,TypeScript也会丢失掉这个信息。更不走运的是,我们可能想要在几个地方重用同样的检查。为了避免这种情况,用户通常必须重复自己的操作或使用类型断言。

在TypeScript 4.4中,情况不再如此,上述例子不会出错。当TypeScript看到我们正在测试一个常量值时,它将做一些额外的工作来查看它是否包含类型保护。如果该类型保护对常量、只读属性或未修改的参数进行操作,那么TypeScript可以适当地缩小该值的范围。

不同类型的类型保护条件被保留下来——不仅仅是typeof的检查。例如,对区别联合类型的检查就可以很优雅:

type Shape =
    | { kind: "circle", radius: number }
    | { kind: "square", sideLength: number };

function area(shape: Shape): number {
    const isCircle = shape.kind === "circle";
    if (isCircle) {
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    }
    else {
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}

另一个例子,这里有一个函数用来检查它的两个输入是否有内容。

function doSomeChecks(
    inputA: string | undefined,
    inputB: string | undefined,
    shouldDoExtraWork: boolean,
) {
    let mustDoWork = inputA && inputB && shouldDoExtraWork;
    if (mustDoWork) {
        // Can access 'string' properties on both 'inputA' and 'inputB'!
        const upperA = inputA.toUpperCase();
        const upperB = inputB.toUpperCase();
        // ...
    }
}

如果mustDoWorktrue,TypeScript可以理解inputAinputB都存在。这意味着我们不必编写像inputA!这样的非空断言来使TypeScript确信inputA不是undefined

另一个优点就是这种分析是传递性的。如果我们给一个条件分配了一个常量,这个条件中有更多的常量,这些常量都是被分配了类型保护,那么TypeScript可以向后传递这些条件。

function f(x: string | number | boolean) {
    const isString = typeof x === "string";
    const isNumber = typeof x === "number";
    const isStringOrNumber = isString || isNumber;
    if (isStringOrNumber) {
        x;  // Type of 'x' is 'string | number'.
    }
    else {
        x;  // Type of 'x' is 'boolean'.
    }
}

需要注意的是,在检查这些条件时,TypeScript不会任意深入,但它的分析对于大多数检查来说已经足够深入了。

这个特性可以让很多直观的JavaScript代码在TypeScript中“正常工作”,而不会妨碍用户。有关更多详细信息,请查看GitHub上的实现。

Symbol和模板字符串模式索引签名

TypeScript允许我们使用索引签名(index signatures)描述每个属性都必须具有特定类型的对象。这允许我们将这些对象用作类似于字典的类型,我们可以使用字符串键以方括号对它们进行索引。

例如,我们可以编写一个带有索引签名的类型,该类型接受字string键并映射到boolean值上。如果我们尝试分配除boolean值以外的任何值,我们将得到一个错误。

interface BooleanDictionary {
    [key: string]: boolean;
}

declare let myDict: BooleanDictionary;

// Valid to assign boolean values
myDict["foo"] = true;
myDict["bar"] = false;

// Error, "oops" isn't a boolean
myDict["baz"] = "oops";

虽然Map在这里可能是一种更好的数据结构(特别是Map<string,boolean>),但是JavaScript对象通常使用起来更方便,或者正好是我们要处理的对象。

类似地,Array<T>已经定义了一个number索引签名,允许我们插入/检索类型T的值。

// This is part of TypeScript's definition of the built-in Array type.
interface Array<T> {
    [index: number]: T;

    // ...
}

let arr = new Array<string>();

// Valid
arr[0] = "hello!";

// Error, expecting a 'string' value here
arr[1] = 123;

索引签名对于在外部表达大量代码时非常有用;然而,到目前为止,它们仅限于stringnumber键(而且string索引签名有一个故意的怪癖,它们可以接受number键,因为它们无论如何都会被强制为字符串)。这意味着TypeScript不允许用symbol键索引对象。TypeScript也不能为某些string键的子集进行索引签名建模,例如,仅仅以data-开头的属性索引签名。

TypeScript 4.4解决了这些限制,并允许对symbol和模板字符串模式进行索引签名。

例如,TypeScript现在允许我们声明一个可以在任意symbol上键入的类型。

interface Colors {
    [sym: symbol]: number;
}

const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");

let colors: Colors = {};

colors[red] = 255;          // Assignment of a number is allowed
let redVal = colors[red];   // 'redVal' has the type 'number'

colors[blue] = "da ba dee"; // Error: Type 'string' is not assignable to type 'number'.

类似地,我们可以使用模板字符串模式类型编写索引签名。这样做的一个用途可能是从TypeScript的多余属性检查中免除以data-开头的属性。当我们将对象文本传递给具有预期类型的对象时,TypeScript将查找预期类型中未声明的多余属性。

interface Options {
    width?: number;
    height?: number;
}

let a: Options = {
    width: 100,
    height: 100,
    "data-blah": true, // Error! 'data-blah' wasn't declared in 'Options'.
};

interface OptionsWithDataProps extends Options {
    // Permit any property starting with 'data-'.
    [optName: `data-${string}`]: unknown;
}

let b: OptionsWithDataProps = {
    width: 100,
    height: 100,
    "data-blah": true,       // Works!

    "unknown-property": true,  // Error! 'unknown-property' wasn't declared in 'OptionsWithDataProps'.
};

关于索引签名的最后一点是,它们现在允许联合类型,只要它们是无限域基元类型的联合—特别是:

  • string
  • number
  • symbol
  • 模板字符串模式(e.g. hello-${string})

其参数是这些类型的并集的索引签名将分解为多个不同的索引签名。

interface Data {
    [optName: string | symbol]: any;
}

// Equivalent to

interface Data {
    [optName: string]: any;
    [optName: symbol]: any;
}

Catch变量中默认为unknown类型(--useUnknownInCatchVariables

在JavaScript中,任何类型的值都可以通过throw抛出,并在catch子句中捕获。因此,TypeScript历史上将catch子句变量类型化为any,并且不允许任何其他类型的注释:

try {
    // Who knows what this might throw...
    executeSomeThirdPartyCode();
}
catch (err) { // err: any
    console.error(err.message); // Allowed, because 'any'
    err.thisWillProbablyFail(); // Allowed, because 'any' :(
}

一旦TypeScript添加了unknown类型,很明显地,对于那些希望获得最高程度的正确性和类型安全性的用户来说,在catch子句unknownany变量都是一个更好的选择,因为它可以更好地缩小范围,并迫使我们针对任意值进行测试。最终,Typescript 4.0允许用户在每个catch子句变量上指定一个unknown(或any)的显式类型注释,这样我们就可以根据具体情况选择更严格的类型;然而,对于某些人来说,在每个catch子句上手动指定unknown是件麻烦事。

这就是为什么TypeScript 4.4引入了一个名为--useUnknownInCatchVariables的新标志。此标志将catch子句变量的默认类型从any更改为unknown

try {
    executeSomeThirdPartyCode();
}
catch (err) { // err: unknown

    // Error! Property 'message' does not exist on type 'unknown'.
    console.error(err.message);

    // Works! We can narrow 'err' from 'unknown' to 'Error'.
    if (err instanceof Error) {
        console.error(err.message);
    }
}

此标志在--strict选项族下启用。这意味着,如果您使用--strict检查代码,此选项将自动启用。在TypeScript 4.4中可能会出现错误,例如

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

如果我们不想在catch子句中处理unknown变量,我们总是可以添加显式的:any注释,这样我们就可以选择不使用更严格的类型。

try {
    executeSomeThirdPartyCode();
}
catch (err: any) {
    console.error(err.message); // Works again!
}

严格的可选属性类型 (--exactOptionalPropertyTypes)

在JavaScript中,读取对象上缺少的属性会产生undefined的值。也可能实际存在一个属性,它的值是undefined。JavaScript中的许多代码倾向于以相同的方式处理这些情况,因此最初TypeScript只是解释每个可选属性,就好像用户在类型中编写了undefined一样。例如,

interface Person {
    name: string,
    age?: number;
}

等同于

interface Person {
    name: string,
    age?: number | undefined;
}

这意味着用户可以显式地编写undefined来代替age

const p: Person = {
    name: "Daniel",
    age: undefined, // This is okay by default.
};

因此,默认情况下,TypeScript不区分值为undefined的现存属性和缺失属性。虽然这在大多数情况下都是有效的,但并非JavaScript中的所有代码都做出相同的假设。像Object.assignObject.keys、对象解构({…obj})和for–in循环这样的函数和操作符,根据对象上的属性是否实际存在而表现不同。在我们的Person示例中,如果当age属性的存在是非常重要的情况下,则这可能会导致运行时错误。

在TypeScript 4.4中,新的标志--exactOptionalPropertyTypes指定可选属性类型应完全按照写入的方式进行解释,这意味着undefined不会添加到类型:

// With 'exactOptionalPropertyTypes' on:
const p: Person = {
    name: "Daniel",
    age: undefined, // Error! undefined isn't a number
};

这个标志不是--strict家族的一部分,如果想要开启这个这个行为,就需要显式地打开它。它还需要启用--strictNullChecks

tsc --help帮助更新和改进

TypeScript的--help选项得到了更新,多亏了宋高的一部分工作,我们引入了一些变化来更新编译器选项的描述,并用一些颜色和其他视觉分隔重新设置--help菜单的样式。虽然我们仍在迭代一些样式,以便跨平台默认主题很好地工作,但你可以通过查看原始建议线程来了解它的外观。

性能提升

更快的声明触发

TypeScript现在对不同上下文中是否可以访问内部符号以及如何打印特定类型进行了缓存。这些更改可以提高TypeScript在具有相当复杂类型的代码中的总体性能,尤其是在发出--declaration标志下的.d.ts文件时。

没翻译明白这块……

更快的路径规范化

TypeScript通常必须对文件路径执行几种类型的“规范化”,以使它们成为编译器可以在任何地方使用的一致格式。这包括用斜杠替换反斜杠,或者删除路径的中间/.//../段。当TypeScript必须在数百万条路径上运行时,这些操作最终会有点慢。在TypeScript 4.4中,首先要对路径进行快速检查,以确定它们首先是否需要任何规范化。这些改进在更大的项目上共同减少了5-10%的加载时间,并且在我们内部测试过的大型项目上有显著的效果。

更快的路径映射

TypeScript现在对它构造路径映射的方式(使用tsconfig.json中的paths选项)进行了缓存,对于具有几百个映射的项目是非常重要的。

使用--strict更快的增量构建

实际上这是一个bug,如果是--strict开启的情况下,在--incremental编译下,TypeScript最终会重做类型检查工作。这导致许多构建的速度都很慢,就像增量构建被关闭了一样。TypeScript 4.4修复了这个问题,不过这个变化也被移植到了TypeScript4.3中。

对大输出进行更快的Source Map生成

TypeScript 4.4添加了一个优化,用于在非常大的输出文件上生成源映射。在构建旧版本的TypeScript编译器时,这将减少大约8%的触发时间。

更快的--force构建

在项目引用上使用--build模式时,TypeScript必须执行最新检查,以确定需要重新构建哪些文件。但是,在执行--force构建时,这些信息无关紧要,因为每个项目依赖项都将从头开始重建。在TypeScript 4.4中,--force构建避免那些不必要的步骤,并开始完整的生成。

JavaScript拼写建议

TypeScript支持在visual studio和visual studio code等编辑器中进行JavaScript编辑。大多数时候,TypeScript试图在JavaScript文件中置身事外;然而,TypeScript通常有大量的信息来提供可信的建议,以及呈现建议的方法,并且不会太具侵入性。

这就是为什么TypeScript现在在普通JavaScript文件中发出拼写建议的原因,这些文件没有//@ts check,或者是在checkJs关闭了的项目中。这些都是TypeScript文件已经有的“Did you mean…?”建议,现在它们以某种形式出现在所有JavaScript文件中。

内嵌提示

TypeScript正在试验对嵌入文本的编辑器支持,它可以帮助在代码中显示有用的信息,比如内联的参数名。你可以把它看作是一种友好的“内嵌文本”。

image.png

不兼容性更改

TypeScript 4.4中lib.d.ts的更改

与每个TypeScript版本一样,lib.d.ts的声明(尤其是为web上下文生成的声明)也发生了变化。您可以查阅我们已知的lib.dom.d.ts更改列表来了解受影响的内容。

在Catch变量中使用unknown

从技术上讲,使用--strict标志运行的用户可能会看到围绕catch变量的新错误是unknown的,特别是在现有代码假定只捕获了Error值的情况下。这通常会导致错误消息,例如:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

为了避免这种情况,您可以特别添加运行时检查,以确保抛出的类型与预期的类型匹配。或者,您可以只使用类型断言,向catch变量添加显式的:any,或者关闭--useUnknownInCatchVariables

抽象属性不允许初始值设定

下面的代码现在会报错,因为抽象属性可能没有初始值设定项:

abstract class C {
    abstract prop = 1;
    //       ~~~~
    // Property 'prop' cannot have an initializer because it is marked abstract.
}

相反地,只能为属性指定类型:

abstract class C {
    abstract prop: number;
}

下一步

目前的目标是在8月中旬发布一个候选版本,并在2021年8月底发布一个稳定的版本。