TypeScript 4.7 RC翻译

736 阅读1分钟

安装:

npm install typescript@rc

从 Beta 到现在有哪些新的功能?

在 beta 版本发布的时候,我们把支持Node ESM作为目标,但是 Node.js 12版本不再维护了,所以我们将稳定版本设置为对标node16。这样就能提供一些新的ES module的功能,(如:pattern trailers)并且也将支持顶级的await

在 beta 版本中,通过/// <reference types="..." />指令resolution-mode语法依旧可用。收到了一些关于import type的反馈并希望重新考虑改功能的设计。以后resolution-mode只能在import type的每日构建测试版本中使用。

在 beta 发布之后,我们意识到在$private字段上使用typeof 会有一些兼容问题。我们也再考虑typeof this.#somePrivate在定义文件输出的时候是否能配合得更好。所以这个功能将不会包含在ts 4.7

此版本还包括一个用于“转到源定义”的新预览编辑器命令。普通的转到定义会跳转到类型定义文件,而不是跳转到javascript或者TypeScript的源代码的地方,但是这个命令可以。

自 beta 版本以来的一些breaking changesstrictNullChecks的严格参数约束和箭头函数解析规则被撤回了,一些看起来无害的改动却在模板字符传中使用JSX ...spread和泛型的时候引入了更严格的规则,这也是一个 breaks

Node.js 支持ECMAScript Module

在过去的几年时间里,Node.js 一直致力于支持 ECMAScript 模块 (ESM)。由于Node.js的生态是建立在Commonjs(CJS)之上,所以要支持ESM是一个非常困难的事。许多新功能需要兼容,导致两种模块规范的相互引用带来了巨大的挑战。Node.js主要是在12及以后的版本开始支持ESM。在TypeScript 4.5中,我们在 Node 中推出了 nightly-only版本对 ESM 支持,以便从用户那里获得一些反馈,并让库作者为更广泛的支持做好准备。

TypeScript 4.7中给配置项module字段新增两个值来支持:node16nodenext

{
  "compilerOptions": {
    "module": "node16"
  }
}

接下来我们将介绍带来的一些新的功能。

package.json中的type和新的扩展

Node.js提供了一个新的配置:type,可选值:“module”和“commonjs”。

{
  "name": "my-package",
  "type": "module",

  "//": "...",
  "dependencies": {}
}

此设置将决定.js文件是解释为 ES 模块还是 CommonJS 模块,未设置默认为CommonJS。与CommonJS相比,ESM有一些新的规则:

  • 可以使用import/export
  • 可以使用顶级await
  • 相对路径导入需要完整的扩展名(import "./foo"必须写成import "./foo.js")
  • node_modules的依赖解析可能会有不同(xb:how?)
  • 一些全局变量如require()process等不能使用。
  • CommonJS需要根据一些特殊的规则导入

我们来看看其中的一些不同。

.ts.tsx在当前的系统中,工作方式一样。当Typescript遇到了.ts.tsx.js或者.jsx文件的时候,将一直往上寻找package.json文件,来确定这些文件是否是一个ESM,并使用它来确定:

  • 如何查找当前文件的导入的其他模块
  • 在生成输出时如何转换文件

.ts文件编译成为ESM的时候,import/export语句将会被保留。当编译为CommonJS的时候,将会产生与现在使用--module commonjs相同的输出。

这也意味着作为 ES 模块的文件和作为 CJS 模块的文件之间的路径解析方式不同。如下:

// ./foo.ts
export function helper() {
  // ...
}

// ./bar.ts
import { helper } from './foo'; // only works in CJS

helper();

由于ESM中相对路径需要完整的扩展名,所以上面的代码在CommonJS中能运行,但是ESM中会运行失败。因此,我们必须要使用foo.ts的输出扩展名来重写。所以bar.ts必须从./foo.js中去导入。

// ./bar.ts
import { helper } from './foo.js'; // works in ESM & CJS

helper();

刚开始的时候这可能感觉有点麻烦,但是一些TypeScript工具和自动导入可以帮助完成。

.d.ts也试用以上规则。

新的文件扩展名

package.json中的type字段能让我们方便的继续使用.ts.js文件扩展名,但是有时候需要编写与type指定的模块模式不一样的模块文件,那就需要指定该文件的模块方式。

Node.js支持了两个新的文件扩展名:.mjs.cjs.mjs总是ESM.cjs总是CommonJS,并且没有任何方式可以改变这个加载方式。

TypeScript也新增支持了两个新的文件扩展名:.mts.cts。当前编译输出javaScript的时候,或生成对应的.mjs.cjs

TypeScript 还支持两个新的声明文件扩展名:.d.mts.d.cts。当为.mts.cts生成声明文件时,会生成对应的.d.mts.d.cts文件。

CommonJS 相互交互

Node.js 允许 ES 模块导入 CommonJS 模块,导入方式跟导入一个具有默认导出的ES模块一样。

// ./foo.cts
export function helper() {
  console.log('hello world!');
}

// ./bar.mts
import foo from './foo.cjs';

// prints "hello world!"
foo.helper();

在某些情况下,Node.js还会从CommonJS模块合成命名导出,这可能会更方便。在这些情况下,ES模块使用“命名空间样式”导入(如:import * as foo from "..."),或者命命名导入(如:import { helper } from "...")。

// ./foo.cts
export function helper() {
  console.log('hello world!');
}

// ./bar.mts
import { helper } from './foo.cjs';

// prints "hello world!"
helper();

Typescript并不总是有办法知道这些命名导入是否会被合成,但是TypeScript会在导入一个Commonjs模块的时候给出一些有用的错误提示。

也可以使用一个Typescript特殊语法导入:

import foo = require('foo');

在一个CommonJS模块中,这相当于是一个require()调用,在ESM中,这个导入会通过createRequire来实现。这样会使代码在浏览器(不支持require())等运行时上的可移植性降低,但是两者之间的相互导入会很有用。可以使用下面的语法编写上面的示例:

// ./foo.cts
export function helper() {
  console.log('hello world!');
}

// ./bar.mts
import foo = require('./foo.cjs');

foo.helper();

最后,从CJS导入ESM的唯一方法就是动态import()

您可以在此处阅读有关 Node 中 ESM/CommonJS 互操作.js 的更多信息

package.json Exports, Imports, 和自引用

Node.js在 package.json 中提供了一个新的字段“exports”来定义入口点。这个字段比package.jsonmain字段会更加有用,并且能控制只导出哪些给使用者。

下面的package.json分别定义了 CommonJSESM 的入口点:

// package.json
{
  "name": "my-package",
  "type": "module",
  "exports": {
    ".": {
      // Entry-point for `import "my-package"` in ESM
      "import": "./esm/index.js",

      // Entry-point for `require("my-package") in CJS
      "require": "./commonjs/index.cjs"
    }
  },

  // CJS fall-back for older versions of Node.js
  "main": "./commonjs/index.cjs"
}

此功能有很多内容,您可以在 Node.js 文档中阅读更多相关信息。在这里,我们将重点关注 TypeScript中如何支持这一特性。

TypeScript 使用的 Node 的原生规则,将会去查找main字段,然后查找该字段对应文件的声明文件。例子:如main指向./lib/index.js,TypeScript 将会去查找./lib/index.d.ts。包作者可以通过types字段来覆盖这一行为(如:"types": "./types/index.d.ts")。

新支持的条件导入与此类似。默认 TypeScript 使用导入条件相同的规则-如果在ESM文件中写了一个import,ts 将会去查找import字段,在CJS中,将会去查找require字段。如果找到了,就会继续查找他们对应的声明文件。如果需要为声明文件指定不同的地址,可以使用types

// package.json
{
  "name": "my-package",
  "type": "module",
  "exports": {
    ".": {
      // Entry-point for `import "my-package"` in ESM
      "import": {
        // Where TypeScript will look.
        "types": "./types/esm/index.d.ts",

        // Where Node.js will look.
        "default": "./esm/index.js"
      },
      // Entry-point for `require("my-package") in CJS
      "require": {
        // Where TypeScript will look.
        "types": "./types/commonjs/index.d.cts",

        // Where Node.js will look.
        "default": "./commonjs/index.cjs"
      }
    }
  },

  // Fall-back for older versions of TypeScript
  "types": "./types/index.d.ts",

  // CJS fall-back for older versions of Node.js
  "main": "./commonjs/index.cjs"
}

请注意,types 条件应始终排在exports第一位。

TypeScript 也以类似的方式支持了package.json的“imports”字段,也支持了模块自引用。这些功能一般不会涉及到,但也支持了。

模块检测控制

一段代码到底是通过“script”引入还是通过模块方式引入,是很模糊的。模块化javaScript代码运行方式有些不一样,有不同的作用域规则,所以工具都必须决定每个文件的运行方式。例如:Node.js导入模块入口点文件写着.mjs,或者最近的package.json文件的"type":"module"。或者在文件中有import或者export语句,那么 TypeScript 就会把这些文件当做模块使用。否则都会认为是做为一个全局作用域使用。

上面的规则与Node.js的行为并不完全的匹配,package.json会更改文件的格式。或者配置jsxreact-jsx,任何 JSX 文件都隐式导入了 JSX 工厂函数。这都不符合现在的期望,并且大多数的 TypeScript 代码都是使用模块化编写。

这就是ts 4.7引入新的配置moduleDetection的原因。moduleDetection取值有:auto(默认)、legacy(与 4.6 及更早版本相同的行为)和force

auto模式下,TypeScript 不仅会查找importexport语句,也会去检测一下情况:

-当运行添加了--module nodenext/--module node16时, package.json中是否有type字段设置为module

  • 当运行运行添加了--jsx react-jsx时,当前文件是否是 JSX 文件。

如果你希望所有的文件都作为模块,设置force将会确保每一个非声明文件都会作为一个模块。无论module, moduleResoluton, 和jsx如何配置,都会作为一个模块。

计算属性流程控制分析

ts 4.7无论是文本类型还是唯一的symbols类型的属性 key 都能正确分析出计算属性的类型。如下:

const key = Symbol();

const numberOrString = Math.random() < 0.5 ? 42 : 'hello';

const obj = {
  [key]: numberOrString,
};

if (typeof obj[key] === 'string') {
  let str = obj[key].toUpperCase();
}

之前这上面的代码会报错,obj[key]的类型是string|number,调用toUpperCase()将会报错。

ts 4.7能正确推断出,obj[key]是一个字符串。

同时也意味着使用配置--strictPropertyInitialization,TypeScript 能正确检测计算属性是否在构造函数中有初始化。

// 'key' has type 'unique symbol'
const key = Symbol();

class C {
  [key]: string;

  constructor(str: string) {
    // oops, forgot to set 'this[key]'
  }

  screamString() {
    return this[key].toUpperCase();
  }
}

ts 4.7之后,--strictPropertyInitialization如果在构造函数中没有初始化[key]属性,那么就会报错。

改进了对象和方法的函数推断

ts 4.7 现在能从对象和数组中的函数做跟精细化的推断。这允许这些函数的类型始终以从左到右的方式流动,就像普通参数一样。

declare function f<T>(arg: {
  produce: (n: string) => T;
  consume: (x: T) => void;
}): void;

// Works
f({
  produce: () => 'hello',
  consume: (x) => x.toLowerCase(),
});

// Works
f({
  produce: (n: string) => n,
  consume: (x) => x.toLowerCase(),
});

// Was an error, now works.
f({
  produce: (n) => n,
  consume: (x) => x.toLowerCase(),
});

// Was an error, now works.
f({
  produce: function () {
    return 'hello';
  },
  consume: (x) => x.toLowerCase(),
});

// Was an error, now works.
f({
  produce() {
    return 'hello';
  },
  consume: (x) => x.toLowerCase(),
});

实例化表达式

一个通用的函数 maxBox:

interface Box<T> {
  value: T;
}

function makeBox<T>(value: T) {
  return { value };
}

基于makeBox,创建一些更加具体功能的函数:

function makeHammerBox(hammer: Hammer) {
  return makeBox(hammer);
}

// or...

const makeWrenchBox: (wrench: Wrench) => Box<Wrench> = makeBox;

上面的代码能运行,但是包裹makeBox有一些浪费,并且编写完整的makeWrenchBox签名变得笨拙。理想情况下,我们只需要一个makeBox的别名来替换所有的泛型签名。

ts 4.7中就可以实现:

依旧在报错..

const makeHammerBox = makeBox<Hammer>;
const makeWrenchBox = makeBox<Wrench>;

通过这样,可以指定makeBox接受更加具体的类型并拒绝其他的。

const makeStringBox = makeBox<string>;

// TypeScript correctly rejects this.
makeStringBox(42);

同样适用于ArrayMapSet的构造函数

// Has type `new () => Map<string, Error>`
const ErrorMap = Map<string, Error>;

// Has type `// Map<string, Error>`
const errorMap = new ErrorMap();

当为函数或构造函数指定类型参数时,它将生成一个新类型,该类型保留具有兼容类型参数列表的所有签名,并将相应的类型参数替换为给定的类型参数。任何其他签名都将被删除,因为 TypeScript 将假定它们不是要使用的。

有关此功能的详细信息,请查看 pr

extends约束infer类型变量

条件类型是一个些高级用户功能。允许我们根据类型的特征去匹配和推断,并基于他们做出推断。例如:如果元祖类型的第一个元素类型是string,那就返回次类型,否则不返回。

type FirstIfString<T> = T extends [infer S, ...unknown[]]
  ? S extends string
    ? S
    : never
  : never;

// string
type A = FirstIfString<[string, number, number]>;

// "hello"
type B = FirstIfString<['hello', number, number]>;

// "hello" | "world"
type C = FirstIfString<['hello' | 'world', boolean]>;

// never
type D = FirstIfString<[boolean, number, string]>;

或者我们可以写成下面这样:

type FirstIfString<T> = T extends [string, ...unknown[]]
  ? // Grab the first type out of `T`
    T[0]
  : never;

上面这个简洁一些,但是有一些“手动”,声明性低。我们不仅要对类型进行模式匹配并为第一个元素命名,还要去获取T的第 0 个元素T[0],如果在处理一些更复杂的类型就会很棘手。

使用嵌套条件来推断类型,然后与该推断的类型进行匹配是很常见的。为了避免第二级嵌套,TypeScript 4.7 现在允许您对任何 infer 类型设置约束。

type FirstIfString<T> =
    T extends [infer S extends string, ...unknown[]]
        ? S
        : never;

通过上面的方法,当 TypeScript 匹配到了S,同时也能确保S是一个string。如果S不是一个string,那就会进入到 false 条件,将返回never

类型参数的可选方差注释

有如下的类型定义

interface Animal {
  animalStuff: any;
}

interface Dog extends Animal {
  dogStuff: any;
}

// ...

type Getter<T> = () => T;

type Setter<T> = (value: T) => void;

假设有两个不同实例的Getter,判断两个不同的 getter 是否可以相互替换完全取决于 T。检测Getter<Dog>Getter<Animal>是否有效,必须检测DogAnimal是否有效。因为 T 的每个类型只是在同一个“方向”上相关,我们说 Getter 类型是 T 的协变。另一方面,检查 Setter<Dog>Setter<Animal>是否有效,涉及到检查 AnimalDog 是否有效。这个方向上的“翻转”有点像数学,检查 −x < −y 和检查 y < x 是一样的。当我们必须像这样翻转方向来比较 T 时,我们说 SetterT 是逆变的。

使用 TypeScript 4.7,我们现在可以显式地指定类型参数的方差。

现在,如果我们想让 GetterT 是协变的,我们可以给它一个out修饰符。

type Getter<out T> = () => T;

类似地,如果我们也想显式地说明 SetterT 是逆变的,我们可以给它一个 in 修饰符。

type Setter<in T> = (value: T) => void;

这里使用 outin,因为类型参数的变化取决于它是用于输出还是用于输入。不用考虑方差,只要考虑 T 是否用于 输出输入 位置即可。

也有同时使用inout的情况。

interface State<in out T> {
    get: () => T;
    set: (value: T) => void;
}

T同时用于输出和输入位置时,它就变成了 不变的 。两个不同的State<T>不能互换,除非它们的 T 相同。换句话说,State<Dog> State<Animal>不能相互替代。

从技术上讲,在纯结构类型系统中,类型参数和它们的变化并不重要—您可以只在每个类型参数的位置插入类型,并检查每个匹配的成员是否在结构上兼容。TypeScript 使用了结构性类型系统,为什么我们对类型参数的变化感兴趣呢?为什么要注释它们呢?

一个原因是,对于代码阅读者来说,能一目了然地了解类型参数是如何使用的。对于很多复杂类型,要判断是可读、可写还是可读写是很困难的。如果忘记了如何使用类型参数,TypeScript 也会进行提示。如下例子,如果忘记在State上指定了outin,将会得到如下错误:

interface State<out T> {
    //          ~~~~~
    // error!
    // Type 'State<sub-T>' is not assignable to type 'State<super-T>' as implied by variance annotation.
    //   Types of property 'set' are incompatible.
    //     Type '(value: sub-T) => void' is not assignable to type '(value: super-T) => void'.
    //       Types of parameters 'value' and 'value' are incompatible.
    //         Type 'super-T' is not assignable to type 'sub-T'.
    get: () => T;
    set: (value: T) => void;
}

另一个原因是精度和速度!TypeScript已经开始尝试通过推断类型参数的变化来进行优化。通过这样做,它可以在合理的时间内对较大的结构类型进行类型检查。提前计算方差允许类型检查器跳过更深入的比较,只比较类型参数,这比反复比较类型的完整结构要快得多。但通常情况下,这种计算仍然相当昂贵,计算可能会发现无法准确解决的循环,这意味着对于类型的方差没有明确的答案。

type Foo<T> = {
  x: T;
  f: Bar<T>;
};

type Bar<U> = (x: Baz<U[]>) => void;

type Baz<V> = {
  value: Foo<V[]>;
};

declare let foo1: Foo<unknown>;
declare let foo2: Foo<string>;

foo1 = foo2; // Should be an error but isn't ❌
foo2 = foo1; // Error - correct ✅

提供显式注释可以加快这些循环的类型检查,并提供更好的准确性。例如,在上面的例子中,将 T 标记为不变量可以帮助停止有问题的赋值。

- type Foo<T> = {
+ type Foo<in out T> = {
      x: T;
      f: Bar<T>;
  }

我们不一定建议用变量注释每个类型参数;例如,有可能(但不推荐)将方差设置得比必要的更严格一些,所以 TypeScript 不会阻止你将一些真正的协变、逆变,甚至独立的东西标记为不变的。因此,如果您选择添加显式的方差标记,我们将鼓励对它们进行深思熟虑和精确的使用。

但是,如果您正在处理深度递归类型,特别是如果您是一个库作者,您可能会有兴趣使用这些注释来为用户带来便利,并在准确性和类型检查速度方面都能获得大的提升,这甚至可能影响他们的代码编辑体验。方差计算何时成为类型检查时间的瓶颈,可以通过实验来确定,并使用像我们的分析跟踪实用程序这样的工具来确定。

关于这个特性更多的详细信息,请参考pr

解析自定义模块后缀

ts 4.7提供一个新的配置项moduleSuffixes来自定模块后缀查找

{
  "compilerOptions": {
    "moduleSuffixes": [".ios", ".native", ""]
  }
}

使用上面的配置,有如下的一个导入:

import * as foo from './foo';

将会去查找以下文件 ./foo.ios.ts, ./foo.native.ts,和 ./foo.tsmoduleSuffixes 中的空字符串""是 TypeScript 查找./foo.ts 所必需的。在某种意义上,moduleSuffix 的默认值是[""]

这个特性对于 React Native 项目很有用,因为每个目标平台都可以使用单独的 tsconfig.json 与不同的模块后缀。

感谢Adam Foxman贡献的moduleSuffixes !

resolution-mode

使用 Node 的 ECMAScript 解析,包含文件的模式和使用的语法决定了如何解析导入;但是从 ECMAScript 模块中引用 CommonJS 模块的类型会很有用,反之亦然。

ts 运行使用/// <reference types="..." />指令:

/// <reference types="pkg" resolution-mode="require" />

// or

/// <reference types="pkg" resolution-mode="import" />

另外,在 TypeScript 的nightly版本中,import type 可以指定一个导入断言来实现类似的功能。

// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from 'pkg' assert { 'resolution-mode': 'require' };

// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from 'pkg' assert { 'resolution-mode': 'import' };

export interface MergedType extends TypeFromRequire, TypeFromImport {}

导入断言也适用于import()

export type TypeFromRequire =
    import("pkg", { assert: { "resolution-mode": "require" } }).TypeFromRequire;

export type TypeFromImport =
    import("pkg", { assert: { "resolution-mode": "import" } }).TypeFromImport;

export interface MergedType extends TypeFromRequire, TypeFromImport {}

import typeimport()提供的 resolution-mode目前支持typescript 的 nightly build版本,可能会得到以下错误:

Resolution mode assertions are unstable. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.

如果您在使用这个功能,请为我们提供反馈

您可以看到引用指令类型导入断言各自的更改。

跳转到源代码定义

TypeScript 4.7 支持一个新的实验性编辑器命令,叫做 Go To Source Definition。它类似于 Go to Definition,但它不会跳转到声明文件中去。这命令将试着去找到正确的实现文件(如:.js.ts

能方便的找到实现文件,而不是.d.ts的声明文件。

您可以在最新版本的 Visual Studio Code 中尝试这个新命令。但是请注意,该功能仍处于预览阶段,并且存在一些已知的限制。在某些情况下,TypeScript 使用启发式来猜测哪个.js 文件对应于某个定义的给定结果,因此这些结果可能不准确。Visual Studio Code 也没有说明结果是否只是猜测,但这是我们正在进行中。

您可以留下关于该功能的反馈,阅读已知的限制,或在我们专门的反馈问题了解更多。

导入分组感知

TypeScript 为 JavaScript 和 TypeScript 都提供了 整理导入 的编辑器特性。但是并不是很好用,而且只是一个简单的排序。

如果要整理下面的导入

// local code
import * as bbb from './bbb';
import * as ccc from './ccc';
import * as aaa from './aaa';

// built-ins
import * as path from 'path';
import * as child_process from 'child_process';
import * as fs from 'fs';

// some code...

将会得到像下面的内容:

// local code
import * as child_process from 'child_process';
import * as fs from 'fs';
// built-ins
import * as path from 'path';
import * as aaa from './aaa';
import * as bbb from './bbb';
import * as ccc from './ccc';

// some code...

这样的结果很不好,当然,我们的导入是按路径排序的,并且保留了注释和换行符,但不是以一种可预测的方式。大多数时候,如果我们以特定的方式对导入进行分组,那么我们希望保持这种方式。

TypeScript 4.7 以一种组感知的方式执行导入排序。在上面的代码中运行它看起来更像你所期望的:

// local code
import * as aaa from './aaa';
import * as bbb from './bbb';
import * as ccc from './ccc';

// built-ins
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

// some code...

我们向提供了这个功能的向Minh Quy 表示感谢。

对象方法片段补全

TypeScript 现在为对象字面量方法提供了代码段补全功能。当给对象中的成员补全时,TypeScript 会为一个方法名提供一个典型的补全条目,同时为整个方法定义提供一个单独的补全条目!

更多详细信息,参考 pr 实现

破坏性更改

lib.d.ts 更新

虽然 TypeScript 努力避免大的更改,但即使是内建库中的小改动也会导致问题。我们期望 DOM 和 lib.d.ts 的更新不会导致重大更改,但可能会有一些小的更改。

JSX 中严格的扩展运算符检测

当在 JSX 中写...spread时,Typescript 将强制检测给定的类型是否为一个对象。因此,类型unknownnever(更少见的是,nullundefined)的值在 JSX 中将不能再使用...spread

如下:

import * as React from 'react';

interface Props {
  stuff?: string;
}

function MyComponent(props: unknown) {
  return <div {...props} />;
}

将会报错如下:

Spread types may only be created from object types.

这使得这种行为与对象字面量的扩展更加一致。

详细信息,参考 github 改动

对模板字符串表达式进行更严格的检查

当一个symbol类型的值在模板字符串中使用时,会触发 JavaScript 的运行时错误。

let str = `hello ${Symbol()}`;
// TypeError: Cannot convert a Symbol value to a string

因此,Typescript 也会抛出一个错误。Typescript 也会去检查一个symbol类型的泛型,在模板字符串中使用。

function logKey<S extends string | symbol>(key: S): S {
  // Now an error.
  console.log(`${key} is the key`);
  return key;
}

function get<T, K extends keyof T>(obj: T, key: K) {
  // Now an error.
  console.log(`Grabbing property '${key}'.`);
  return obj[key];
}

Typescript 将抛出如下错误:

Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.

可以通过调用String方法来避免错误。

function logKey<S extends string | symbol>(key: S): S {
  // Now an error.
  console.log(`${String(key)} is the key`);
  return key;
}

在一些情况下,这个错误会非常的死板,当使用keyof的是时候,可能并不关心symbol的属性。这时可以使用string & keyof

function get<T, K extends string & keyof T>(obj: T, key: K) {
  // Now an error.
  console.log(`Grabbing property '${key}'.`);
  return obj[key];
}

更多信息,请查看 PR

LanguageServiceHost 的 readFile 方法必传

如果您正在创建 LanguageService 实例,那么提供的 LanguageServiceHosts 将需要提供一个 readFile 方法。为了支持新的 moduleDetection 编译器选项,这个更改是必要的。

关于这个修改可参考

只读元祖的length属性只读

只读元祖的length属性将会是只读的。这在固定长度的元祖中不会有啥问题,但对于末尾可选元素和 rest 元素类型的元组会被忽略。

因此,下面的代码就会运行失败:

function overwriteLength(tuple: readonly [string, string, string]) {
  // Now errors.
  tuple.length = 7;
}

更多信息查看这