TypeScript 4.0 RC发布,带来诸多更新

2020-08-21 15:39:55 浏览数 (1)

作者 | Daniel Rosenwasser

译者 | 王强

策划 | 李俊辰

8 月 6 日,微软发布了 TypeScript 4.0 的 RC 版本。本文是官方新闻稿的全文翻译,

随着这个 RC 版本的发布,我们离 TypeScript 的下一个主要版本也越来越近了。但请不要担心,这个版本并没有加入特别重大的更改。我们的 TypeScript 进化理念一直没变,就是为开发人员提供一种升级路径,既能最大程度地减少重大更改的数量,同时仍提供一定的灵活性,以在合适的时间将可疑代码标记为错误。因此,我们将继续使用与以前版本相似的版本控制模型,也就是说 4.0 会是 TypeScript 3.9 的自然延续。

要开始使用 RC,可以通过 NuGet 获取,或使用以下 npm 命令:

代码语言:javascript复制
npm install typescript@rc

你还可以通过以下方式获得编辑器支持

  • 下载 Visual Studio 2019/2017 支持:
    • https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.TypeScript-40rc
  • 遵循 Visual Studio Code 和 Sublime Text 的说明。

下面我们就来看看 TypeScript 4.0 都有哪些特性!

可变元组类型

考虑 JavaScript 中称为 concat 的函数,该函数接收两个数组或元组类型,并将它们连接在一起以创建一个新数组。

代码语言:javascript复制
function concat(arr1, arr2) {
    return [...arr1, ...arr2];
}

考虑 tail,它接收一个数组或元组,并返回除第一个元素外的所有元素。

代码语言:javascript复制
function tail(arg) {
    const [_, ...result] = arg;
    return result
}

我们如何在 TypeScript 中为它们类型化?对于 concat,我们在较旧版本的 TS 中唯一可以做的就是尝试编写一些重载。

代码语言:javascript复制
function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

这是第二个数组始终为空时的七个重载。当 arr2 有一个参数时再加一些。

代码语言:javascript复制
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

很明显这变得越来越离谱了。不幸的是,在类型化 tail 之类的函数时,你也会遇到同样的问题。

下面是另一种情况,我们称之为“被一千个重载搞垮”,它甚至什么问题都解决不了。它只为我们想写的重载提供正确的类型(不管重载有多少)。如果我们想做一个 catch-all,则需要下面的重载:

代码语言:javascript复制
function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;

但在使用元组时,这个签名不会包含输入的长度或元素的顺序的任何信息。TypeScript 4.0 带来了两个基本更改,并在推断方面进行了改进,从而可以类型化这些内容。

第一个变化是元组类型语法中的 spread 现在可以泛型。这意味着即使我们不知道要操作的实际类型,也可以表示对元组和数组的高阶操作。在这些元组类型中实例化泛型 spread(或用真实类型替换)时,它们可以产生其他数组和元组类型集。

例如,我们可以类型化 tail 那样的函数,而不会出现“一千个重载死亡”的问题。

代码语言:javascript复制
function tail<T extends any[]>(arr: readonly [any, ...T]) {
    const [_ignored, ...rest] = arr;
    return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
// type [2, 3, 4]
const r1 = tail(myTuple);
// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);

第二个变化是,spread 元素可以出现在元组中的任何位置,而不仅仅是在结尾!

代码语言:javascript复制
type Strings = [string, string];
type Numbers = [number, number];
// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];

以前,TypeScript 会发出如下错误。

代码语言:javascript复制
A rest element must be last in a tuple type.

但是现在可以在任何位置放置 spread。当我们 spread 没有已知长度的类型时,结果类型也将变得不受限制,并且所有连续元素都会分解为结果的 rest 元素类型。

代码语言:javascript复制
type Strings = [string, string];
type Numbers = number[]
// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];

将这两种行为结合在一起,我们可以为 concat 编写一个类型良好的签名:

代码语言:javascript复制
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
    return [...arr1, ...arr2];
}

尽管一个签名仍然有些冗长,但它毕竟只有一个,只需写一次,并且在所有数组和元组上都具有可预测的行为。这个功能很棒,但是也有其他更复杂的场景。例如,考虑一个函数来部分应用参数,名为 partialCall。partialCall 接收一个函数以及该函数期望的几个初始参数。然后,它返回一个新函数,接收它需要的其他所有参数,并一起调用它们。

代码语言:javascript复制
function partialCall(f, ...headArgs) {
    return (...tailArgs) => f(...headArgs, ...tailArgs)
}

TypeScript 4.0 改进了 rest 参数和 rest 元组元素的推断过程,因此我们可以类型化它并使其“正常工作”。

代码语言:javascript复制
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(f: (...args: [...T, ...U]) => R, ...headArgs: T) {
    return (...b: U) => f(...headArgs, ...b)
}

在这种情况下,partialCall 会知道其最初可以使用和不能使用哪些参数,并返回一个可以正确接收和拒绝剩余内容的函数。

代码语言:javascript复制
const foo = (x: string, y: number, z: boolean) => {}
// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
// ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.

// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops")
// ~~~~~~
// error! Expected 4 arguments, but got 5.

// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");
// What can we do with f3 now?
f3(123, true); // works!
f3();
// error! Expected 2 arguments, but got 0.
f3(123, "hello");
// ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'.

可变元组类型创造了许多新模式,尤其是在函数组合方面。我们希望利用它来改善对 JavaScript 内置的 bind 方法的类型检查。此外还有其他一些推断改进和模式,想了解更多信息,可以查看可变元组的拉取请求。

https://github.com/microsoft/TypeScript/pull/39094

标记的元组元素

改善元组类型和参数列表的体验很重要,因为它使我们能够围绕常见的 JavaScript 习惯用法进行强类型验证——实际上只是对参数列表进行切片和切块,并将它们传递给其他函数。使用元组类型作为 rest 参数是其中的关键。

例如,以下函数使用元组类型作为 rest 参数:

代码语言:javascript复制
function foo(...args: [string, number]): void {
    // ...
}

……应该与以下函数没有区别……

代码语言:javascript复制
function foo(arg0: string, arg1: number): void {
    // ...
}

……对于 foo 的任何调用者。

代码语言:javascript复制
foo("hello", 42); // works
foo("hello", 42, true); // error
foo("hello"); // error

不过可读性就有区别了。在第一个示例中,我们没有第一个和第二个元素的参数名称。尽管这些对类型检查没有影响,但元组位置上缺少标记会难以传达我们的意图。因此,在 TypeScript 4.0 中,元组类型现在可以提供标记。

代码语言:javascript复制
type Range = [start: number, end: number];

为了进一步加强参数列表和元组类型之间的联系,我们让 rest 元素和可选元素的语法与参数列表的语法一致。

代码语言:javascript复制
type Foo = [first: number, second?: string, ...rest: any[]];

在标记一个元组元素时,还必须标记元组中的所有其他元素。

代码语言:javascript复制
type Bar = [first: string, number];
// ~~~~~~
// error! Tuple members must all have names or all not have names.

值得注意的是,标记在解构时不需要用不同的名称命名变量。它们纯粹是为文档和工具链服务的。

代码语言:javascript复制
function foo(x: [first: string, second: number]) {
    // ...
    // note: we didn't need to name these 'first' and 'second'
    let [a, b] = x;
    // ...
}

总的来说,当使用围绕元组和参数列表的模式,以及以类型安全的方式实现重载时,带标记的元组非常方便。了解更多信息,请查看带标记的元组元素的拉取请求。

https://github.com/microsoft/TypeScript/pull/38234

构造器的类属性推断

当启用 noImplicitAny 时,TypeScript 4.0 现在可以使用控制流分析来确定类中属性的类型。

代码语言:javascript复制
class Square {
    // Previously: implicit any!
    // Now: inferred to `number`!
    area;
    sideLength;
    constructor(sideLength: number) {
        this.sideLength = sideLength;
        this.area = sideLength ** 2;
    }
}

如果构造器的路径并非都分配给实例成员,则该属性可能被认为是 undefined 的。

代码语言:javascript复制
class Square {
    sideLength;
    constructor(sideLength: number) {
        if (Math.random()) {
            this.sideLength = sideLength;
        }
    }
    get area() {
        return this.sideLength ** 2;
        // ~~~~~~~~~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}

如果你更了解某些情况(例如,你拥有某种 initialize 方法),则当你处于 strictPropertyInitialization 中时,需要使用显式类型注释以及明确的赋值断言(!)。

代码语言:javascript复制
class Square {
    // definite assignment assertion
    // v
    sideLength!: number;
    // ^^^^^^^^
    // type annotation
    constructor(sideLength: number) {
        this.initialize(sideLength)
    }
    initialize(sideLength: number) {
        this.sideLength = sideLength;
    }
    get area() {
        return this.sideLength ** 2;
    }
}

短路赋值运算符

JavaScript 和许多语言都支持一组称为"复合赋值运算符"的运算符。复合赋值运算符将一个运算符应用于两个参数,然后将结果赋给左侧。你可能以前看过这些:

代码语言:javascript复制
// Addition
// a = a   b
a  = b;
// Subtraction
// a = a - b
a -= b;
// Multiplication
// a = a * b
a *= b;
// Division
// a = a / b
a /= b;
// Exponentiation
// a = a ** b
a **= b;
// Left Bit Shift
// a = a << b
a <<= b;

JavaScript 中有很多运算符都有对应的赋值运算符!但是有三个值得注意的例外:逻辑和(&&),逻辑或(||)和空值合并(??)。TypeScript 4.0 添加了三个新的赋值运算符:&&=,||= 和??=。

这些运算符非常适合替换下面这种代码示例:

代码语言:javascript复制
a = a && b;
a = a || b;
a = a ?? b;

我们甚至看到了一些模式,可以在需要时懒惰地初始化值。

代码语言:javascript复制
let values: string[];
// Before
(values ?? (values = [])).push("hello");
// After
(values ??= []).push("hello");

在极少数情况下,你使用带有副作用的 getter 或 setter 时,需要注意的是这些运算符仅在必要时执行赋值。从这个意义上讲,赋值是短路的,这是它们与其他复合赋值唯一的区别。

代码语言:javascript复制
a ||= b;
// actually equivalent to
a || (a = b);

有关更多细节可以查看拉取请求:

https://github.com/microsoft/TypeScript/pull/37727

你也可以查看 TC39 的提案存储库:

https://github.com/tc39/proposal-logical-assignment/

catch 子句支持 unknown

自 TypeScript 诞生以来,catch 子句变量始终按 any 类型化。这意味着 TypeScript 允许你对它们进行任何操作。

代码语言:javascript复制
try {
    // ...
}
catch (x) {
    // x has type 'any' - have fun!
    console.log(x.message);
    console.log(x.toUpperCase());
    x  ;
    x.yadda.yadda.yadda();
}

上述代码会有一些无法预期的行为!由于这些变量默认情况下的类型为 any,因此它们没有任何类型安全性可以防止无效操作。因此,TypeScript 4.0 现在允许你将 catch 子句变量的类型指定为 unknown。unknown 比 any 更安全,因为它会在我们操作值之前提醒我们执行某种类型检查。

代码语言:javascript复制
try {
    // ...
}
catch (e: unknown) {
    // error!
    // Property 'toUpperCase' does not exist on type 'unknown'.
    console.log(e.toUpperCase());
    if (typeof e === "string") {
        // works!
        // We've narrowed 'e' down to the type 'string'.
        console.log(e.toUpperCase());
    }
}

尽管默认情况下 catch 变量的类型不会更改,但我们将来可能会考虑使用新的 --strict 模式标志,以便用户选择启用此行为。同时,应该可以编写一个 lint 规则来强制 catch 变量具有显式注释: any 或: unknown。有关更多信息,可以查看拉取请求。

https://github.com/microsoft/TypeScript/pull/39015

定制 JSX 工厂

使用 JSX 时,fragment 是 JSX 元素的一种,允许我们返回多个子元素。当我们第一次在 TypeScript 中实现 fragment 时,我们对其他库如何利用它们并不了解。如今,大多数鼓励使用 JSX 和支持 fragment 的库都具有类似的 API 设计。

在 TypeScript 4.0 中,用户可以通过新的 jsxFragmentFactory 选项来自定义 fragment 工厂。

例如,以下 tsconfig.json 文件告诉 TypeScript 以与 React 兼容的方式转换 JSX,但将每个调用切换为 h 而不是 React.createElement,并使用 Fragment 而不是 React.Fragment。

代码语言:javascript复制
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

如果需要基于各个文件使用不同的 JSX 工厂,则可以利用新的 /**@jsxFrag*/注释。例如,下面的内容:

代码语言:javascript复制
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = <>
    <div>Hello</div>
</>;

将输出:

代码语言:javascript复制
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null,
    h("div", null, "Hello"));

查看拉取请求以获取更多信息。

https://github.com/microsoft/TypeScript/pull/38720

加快了 build 模式的速度

以前,使用 --noEmitOnError 标志时,当先前的编译在 --incremental 下出现错误,编译速度将非常缓慢。这是因为基于 --noEmitOnError 标志,上次编译的任何信息都不会缓存在.tsbuildinfo 文件中。

TypeScript 4.0 对此进行了更改,从而在这些情况下极大地提高了速度,进而改进了 --build 模式的场景(这意味着同时有 --incremental 和 --noEmitOnError)。

有关详细信息,请查看拉取请求。

https://github.com/microsoft/TypeScript/pull/38853

带有 --noEmit 的 --incremental

TypeScript 4.0 允许我们在利用 --incremental 编译时使用 --noEmit 标志。以前不允许这样做,因为 --incremental 需要发出.tsbuildinfo 文件。

有关详细信息,请查看拉取请求。

https://github.com/microsoft/TypeScript/pull/39122

编辑器改进

TypeScript 编译器不仅可以为大多数主流编辑器提供较好的 TS 编辑体验,还可以改进 Visual Studio 系列编辑器的 JavaScript 体验。

根据你使用的编辑器,在编辑器中使用新的 TypeScript/JavaScript 功能时会有区别:

  • Visual Studio Code 支持选择不同版本的 TypeScript。另外,还有 JavaScript/TypeScript Nightly Extension(通常非常稳定)。
  • Visual Studio 2017/2019 具有 [上面的 SDK 安装程序] 和 MSBuild 安装。
  • Sublime Text 3 支持选择不同版本的 TypeScript。

更多信息见 TS 编辑器支持列表。

https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support

转换为可选链

可选链是一项新功能,受到了广泛的欢迎。TypeScript 4.0 在转换常见模式时可以利用可选链和空值合并的优势!

我们认为这种重构应该能捕获大多数用例的意图,尤其是当 TypeScript 对你的类型有更精确的了解时。

有关详细信息,请查看拉取请求。

https://github.com/microsoft/TypeScript/pull/39135

/**@deprecated*/ 支持

现在,TypeScript 的编辑支持可以识别声明中是否带有 /**@deprecated* JSDoc 注释。该信息显示在自动完成列表中,并作为编辑器可以特别处理的建议诊断。在像 VSCode 这样的编辑器中,deprecated 的值通常显示为删除线样式。

有关详细信息,查看拉取请求。

https://github.com/microsoft/TypeScript/pull/38523

启动时的部分编辑模式

很多用户抱怨启动时间缓慢,尤其是在大型项目中。具体来说,罪魁祸首通常是一个称为项目加载的过程,该过程与我们编译器的程序构建步骤大致相同。这一过程从一组初始文件开始,解析它们、解析它们的依赖、再解析那些依赖,解析那些依赖的依赖,等等,最后需要花费很长时间。项目越大,启动延迟可能会越长。

所以我们一直在努力为开发人员提供一种新的模式,在获得完整的语言服务体验之前提供部分体验。这里的核心思想是,编辑者可以运行仅具有单个文件视图的轻量级部分服务器。这一直是编辑器的一种选项,但是 TypeScript 4.0 将功能扩展到了语义操作(与仅语法操作相对)。虽然这意味着服务器的信息有限(因此并非每个操作都将完全完成),但当你首次打开编辑器时,一些基本的代码完成、快速信息、签名帮助和快速定义通常就足够了。

这种新模式可以将 TypeScript 在代码库上开始交互之前的准备时间从 20 秒到 1 分钟缩短到 2-5 秒之间。

当前,唯一支持此模式的编辑器是 Visual Studio Code Insiders,你可以按照以下步骤尝试。

  1. 安装 VisualStudioCodeInsiders;
  2. 配置 Visual Studio CodeInsiders 以使用 RC,或为 Visual Studio Code Insiders 安装 JavaScript 和 TypeScript Nightly Extension。

从编辑器和语言支持两个方面来看,UX 和功能仍有改进的余地。例如,虽然编辑器已经加载并运行了部分编辑支持,但你仍会在状态栏中看到“Initializing JS/TS language features”。你可以忽略它。我们还列出了准备加入的改进,希望获得更多反馈。

https://github.com/microsoft/TypeScript/issues/39035

有关更多信息,你可以查看原始提案,拉取请求,以及后续的 meta 问题。

https://github.com/microsoft/TypeScript/issues/37713

更智能的自动导入

自动导入是一个了不起的功能。但是,自动导入在用 TypeScript 编写的包上不起作用——也就是说,我们得在项目的其他位置至少写了一个显式导入。

为什么自动导入适用于 @types 软件包,而不适用于使用自己类型的包呢?其实自动导入是通过检查项目中已经包含的软件包来实现的。TypeScript 有一个怪癖,可以自动包括 node_modules/@types 中的所有包,而忽略其他包;但爬取所有 node_modules 包的开销可能会很昂贵。

当你尝试自动导入刚刚安装但尚未使用的内容时,这些都会导致糟糕的体验。

TypeScript 4.0 现在可以包含你在 package.json 的 dependencies 字段中列出的包。这些包中的信息仅用于改进自动导入,不会更改类型检查等其他内容。这有助于减轻遍历 node_modules 目录的成本,同时解决上面的大问题。

有关详细信息,可以查看提案问题。

https://github.com/microsoft/TypeScript/issues/37812

我们的新网站!

TypeScript 网站最近被彻底重写了!

详细信息可以参考之前的文章:

《TypeScript 新版网站上线:带来了新的导航机制》

重大更改

lib.d.ts

我们的 lib.d.ts 声明已更改,具体来说是 DOM 的类型已更改。主要是删除了 document.origin,它仅在 IE 的旧版本中有效,而 Safari MDN 建议改用 self.origin。

属性重写访问器(反之亦然)是错误

以前,只有在使用 useDefineForClassFields 时,属性重写访问器或访问器重写属性是一个错误;但现在,在派生类中声明一个将重写基类中的 getter 或 setter 的属性时总是发出错误。

代码语言:javascript复制
class Base {
    get foo() {
        return 100;
    }
    set foo() {
        // ...
    }
}
class Derived extends Base {
    foo = 
10;
//  ~~~
// error!
// 'foo' is defined 
as an accessor 
in class 'Base',
// but 
is overridden here 
in 'Derived' as an instance property.
}
代码语言:javascript复制
class Base {
    prop = 10;
}
class Derived extends Base {
    get prop() {
    // ~~~~
    // error!
    // 'prop' is defined as a property in class 'Base', but is overridden here in 'Derived' as an accessor.
        return 100;
    }
}

有关详细信息,查看拉取请求。

https://github.com/microsoft/TypeScript/pull/37894

delete 删除的属性必须是可选的。

在 strictNullChecks 中使用 delete 运算符时,操作数现在必须为 any、unknown、never 或为可选(因为它在类型中包含 undefined)。否则,使用 delete 运算符是错误的。

代码语言:javascript复制
interface Thing {
    prop: string;
}
function f(x: Thing) {
    delete x.prop;
    // ~~~~~~
    // error! The operand of a 'delete' operator must be optional.
}

关于更多信息,查看拉取请求。

https://github.com/microsoft/TypeScript/pull/37921

TypeScript 的 Node 工厂用法已弃用

如今,TypeScript 提供了一组用于生成 AST 节点的“工厂”函数。但是,TypeScript 4.0 提供了新的 node 工厂 API。因此 TypeScript 4.0 决定弃用使用这些旧函数,推荐改用新函数。

有关更多信息,请查看拉取请求。

https://github.com/microsoft/TypeScript/pull/35282

下一步计划

随着我们接近稳定版本,我们正在寻找用户帮助我们测试。我们希望能让 TypeScript 4.0 尽可能完美,因此请尝试一下并给 TypeScript 团队提供反馈。

https://github.com/microsoft/TypeScript/issues

0 人点赞