安装:
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 changes,strictNullChecks的严格参数约束和箭头函数解析规则被撤回了,一些看起来无害的改动却在模板字符传中使用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字段新增两个值来支持:node16和nodenext
{
"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.json的main字段会更加有用,并且能控制只导出哪些给使用者。
下面的package.json分别定义了 CommonJS 和 ESM 的入口点:
// 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会更改文件的格式。或者配置jsx为react-jsx,任何 JSX 文件都隐式导入了 JSX 工厂函数。这都不符合现在的期望,并且大多数的 TypeScript 代码都是使用模块化编写。
这就是ts 4.7引入新的配置moduleDetection的原因。moduleDetection取值有:auto(默认)、legacy(与 4.6 及更早版本相同的行为)和force
在auto模式下,TypeScript 不仅会查找import和export语句,也会去检测一下情况:
-当运行添加了--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);
同样适用于Array、Map和Set的构造函数
// 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>是否有效,必须检测Dog→ Animal是否有效。因为 T 的每个类型只是在同一个“方向”上相关,我们说 Getter 类型是 T 的协变。另一方面,检查 Setter<Dog>→Setter<Animal>是否有效,涉及到检查 Animal→Dog 是否有效。这个方向上的“翻转”有点像数学,检查 −x < −y 和检查 y < x 是一样的。当我们必须像这样翻转方向来比较 T 时,我们说 Setter 对 T 是逆变的。
使用 TypeScript 4.7,我们现在可以显式地指定类型参数的方差。
现在,如果我们想让 Getter 对 T 是协变的,我们可以给它一个out修饰符。
type Getter<out T> = () => T;
类似地,如果我们也想显式地说明 Setter 对 T 是逆变的,我们可以给它一个 in 修饰符。
type Setter<in T> = (value: T) => void;
这里使用 out 和 in,因为类型参数的变化取决于它是用于输出还是用于输入。不用考虑方差,只要考虑 T 是否用于 输出 和 输入 位置即可。
也有同时使用in和out的情况。
interface State<in out T> {
get: () => T;
set: (value: T) => void;
}
当T同时用于输出和输入位置时,它就变成了 不变的 。两个不同的State<T>不能互换,除非它们的 T 相同。换句话说,State<Dog>和 State<Animal>不能相互替代。
从技术上讲,在纯结构类型系统中,类型参数和它们的变化并不重要—您可以只在每个类型参数的位置插入类型,并检查每个匹配的成员是否在结构上兼容。TypeScript 使用了结构性类型系统,为什么我们对类型参数的变化感兴趣呢?为什么要注释它们呢?
一个原因是,对于代码阅读者来说,能一目了然地了解类型参数是如何使用的。对于很多复杂类型,要判断是可读、可写还是可读写是很困难的。如果忘记了如何使用类型参数,TypeScript 也会进行提示。如下例子,如果忘记在State上指定了out和in,将会得到如下错误:
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.ts。 moduleSuffixes 中的空字符串""是 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 type 和 import()提供的 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...
对象方法片段补全
TypeScript 现在为对象字面量方法提供了代码段补全功能。当给对象中的成员补全时,TypeScript 会为一个方法名提供一个典型的补全条目,同时为整个方法定义提供一个单独的补全条目!
更多详细信息,参考 pr 实现
破坏性更改
lib.d.ts 更新
虽然 TypeScript 努力避免大的更改,但即使是内建库中的小改动也会导致问题。我们期望 DOM 和 lib.d.ts 的更新不会导致重大更改,但可能会有一些小的更改。
JSX 中严格的扩展运算符检测
当在 JSX 中写...spread时,Typescript 将强制检测给定的类型是否为一个对象。因此,类型unknown和never(更少见的是,null和undefined)的值在 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;
}