TypeScript 2.7

常量命名属性

¥Constant-named properties

TypeScript 2.7 增加了对在类型(包括 ECMAScript 符号)上声明 const 命名属性的支持。

¥TypeScript 2.7 adds support for declaring const-named properties on types including ECMAScript symbols.

示例

¥Example

ts
// Lib
export const SERIALIZE = Symbol("serialize-method-key");
export interface Serializable {
[SERIALIZE](obj: {}): string;
}
ts
// consumer
import { SERIALIZE, Serializable } from "lib";
class JSONSerializableItem implements Serializable {
[SERIALIZE](obj: {}) {
return JSON.stringify(obj);
}
}

这也适用于数字和字符串字面量。

¥This also applies to numeric and string literals.

示例

¥Example

ts
const Foo = "Foo";
const Bar = "Bar";
let x = {
[Foo]: 100,
[Bar]: "hello"
};
let a = x[Foo]; // has type 'number'
let b = x[Bar]; // has type 'string'

unique symbol

为了能够将符号视为唯一字面量,一个新的类型 unique symbol 可用。unique symbolsymbol 的子类型,仅通过调用 Symbol()Symbol.for() 或显式类型注释产生。新类型仅允许在 const 声明和 readonly static 属性中使用,并且为了引用特定的唯一符号,你必须使用 typeof 运算符。每个对 unique symbol 的引用都隐含着一个与给定声明绑定的完全唯一的标识。

¥To enable treating symbols as unique literals a new type unique symbol is available. unique symbol is a subtype of symbol, and are produced only from calling Symbol() or Symbol.for(), or from explicit type annotations. The new type is only allowed on const declarations and readonly static properties, and in order to reference a specific unique symbol, you’ll have to use the typeof operator. Each reference to a unique symbol implies a completely unique identity that’s tied to a given declaration.

示例

¥Example

ts
// Works
declare const Foo: unique symbol;
// Error! 'Bar' isn't a constant.
let Bar: unique symbol = Symbol();
// Works - refers to a unique symbol, but its identity is tied to 'Foo'.
let Baz: typeof Foo = Foo;
// Also works.
class C {
static readonly StaticSymbol: unique symbol = Symbol();
}

因为每个 unique symbol 都有一个完全独立的标识,所以没有两个 unique symbol 类型可以分配或相互比较。

¥Because each unique symbol has a completely separate identity, no two unique symbol types are assignable or comparable to each other.

示例

¥Example

ts
const Foo = Symbol();
const Bar = Symbol();
// Error: can't compare two unique symbols.
if (Foo === Bar) {
// ...
}

严格的类初始化

¥Strict Class Initialization

TypeScript 2.7 引入了一个名为 strictPropertyInitialization 的新标志。此标志执行检查以确保类的每个实例属性都在构造函数主体中初始化,或通过属性初始化器初始化。例如

¥TypeScript 2.7 introduces a new flag called strictPropertyInitialization. This flag performs checks to ensure that each instance property of a class gets initialized in the constructor body, or by a property initializer. For example

ts
class C {
foo: number;
bar = "hello";
baz: boolean;
// ~~~
// Error! Property 'baz' has no initializer and is not definitely assigned in the
// constructor.
constructor() {
this.foo = 42;
}
}

在上图中,如果我们真的希望 baz 可能是 undefined,我们应该将其声明为类型 boolean | undefined

¥In the above, if we truly meant for baz to potentially be undefined, we should have declared it with the type boolean | undefined.

在某些情况下,属性可以间接初始化(例如通过辅助方法或依赖注入库),在这种情况下,你可以使用新的明确赋值断言修饰符来初始化属性(如下所述)。

¥There are certain scenarios where properties can be initialized indirectly (perhaps by a helper method or dependency injection library), in which case you can use the new definite assignment assertion modifiers for your properties (discussed below).

ts
class C {
foo!: number;
// ^
// Notice this '!' modifier.
// This is the "definite assignment assertion"
constructor() {
this.initialize();
}
initialize() {
this.foo = 0;
}
}

请记住,strictPropertyInitialization 将与其他 strict 模式标志一起启用,这可能会影响你的项目。你可以在 tsconfig.jsoncompilerOptions 中将 strictPropertyInitialization 设置为 false,或在命令行中将其设置为 --strictPropertyInitialization false,以关闭此检查。

¥Keep in mind that strictPropertyInitialization will be turned on along with other strict mode flags, which can impact your project. You can set the strictPropertyInitialization setting to false in your tsconfig.json’s compilerOptions, or --strictPropertyInitialization false on the command line to turn off this checking.

明确赋值断言

¥Definite Assignment Assertions

明确赋值断言是一项功能,它允许在实例属性和变量声明之后放置 !,以便向 TypeScript 传达变量确实已赋值(即使 TypeScript 的分析无法检测到)。

¥The definite assignment assertion is a feature that allows a ! to be placed after instance property and variable declarations to relay to TypeScript that a variable is indeed assigned for all intents and purposes, even if TypeScript’s analyses cannot detect so.

示例

¥Example

ts
let x: number;
initialize();
console.log(x + x);
// ~ ~
// Error! Variable 'x' is used before being assigned.
function initialize() {
x = 10;
}

使用明确赋值断言,我们可以通过在 x 声明后附加 ! 来断言 x 确实已赋值:

¥With definite assignment assertions, we can assert that x is really assigned by appending an ! to its declaration:

ts
// Notice the '!'
let x!: number;
initialize();
// No error!
console.log(x + x);
function initialize() {
x = 10;
}

从某种意义上说,明确赋值断言运算符是非空断言运算符(其中表达式以 ! 为后缀)的对偶,我们也可以在示例中使用后者。

¥In a sense, the definite assignment assertion operator is the dual of the non-null assertion operator (in which expressions are post-fixed with a !), which we could also have used in the example.

ts
let x: number;
initialize();
// No error!
console.log(x! + x!);
function initialize() {
x = 10;
}

在我们的示例中,我们知道所有使用 x 的情况都将被初始化,因此使用明确赋值断言比使用非空断言更有意义。

¥In our example, we knew that all uses of x would be initialized so it makes more sense to use definite assignment assertions than non-null assertions.

固定长度元组

¥Fixed Length Tuples

在 TypeScript 2.6 及更早版本中,[number, string, string] 被视为 [number, string] 的子类型。这是由 TypeScript 的结构特性所驱动的;[number, string, string] 的第一个和第二个元素分别是 [number, string] 的第一个和第二个元素的子类型。然而,在研究了元组的实际使用情况后,我们发现,大多数允许这样做的情况通常是不可取的。

¥In TypeScript 2.6 and earlier, [number, string, string] was considered a subtype of [number, string]. This was motivated by TypeScript’s structural nature; the first and second elements of a [number, string, string] are respectively subtypes of the first and second elements of [number, string]. However, after examining real world usage of tuples, we noticed that most situations in which this was permitted was typically undesirable.

在 TypeScript 2.7 中,不同元数的元组不再可以相互赋值。得益于 Kiara Grouwstra 的拉取请求,元组类型现在将其元数编码到其各自 length 属性的类型中。这是通过利用数字字面量类型来实现的,该类型现在允许元组与不同元数的元组区分开来。

¥In TypeScript 2.7, tuples of different arities are no longer assignable to each other. Thanks to a pull request from Kiara Grouwstra, tuple types now encode their arity into the type of their respective length property. This is accomplished by leveraging numeric literal types, which now allow tuples to be distinct from tuples of different arities.

从概念上讲,你可以认为类型 [number, string] 等同于以下 NumStrTuple 的声明:

¥Conceptually, you might consider the type [number, string] to be equivalent to the following declaration of NumStrTuple:

ts
interface NumStrTuple extends Array<number | string> {
0: number;
1: string;
length: 2; // using the numeric literal type '2'
}

请注意,这对某些代码来说是一个重大更改。如果你需要恢复元组仅强制最小长度的原始行为,则可以使用类似的声明,但不显式定义 length 属性,而是回退到 number

¥Note that this is a breaking change for some code. If you need to resort to the original behavior in which tuples only enforce a minimum length, you can use a similar declaration that does not explicitly define a length property, falling back to number.

ts
interface MinimumNumStrTuple extends Array<number | string> {
0: number;
1: string;
}

请注意,这并不意味着元组表示不可变数组,而是一种隐含的约定。

¥Note that this does not imply tuples represent immutable arrays, but it is an implied convention.

改进了对象字面量的类型推断

¥Improved type inference for object literals

TypeScript 2.7 改进了在同一上下文中出现多个对象字面量的类型推断。当多个对象字面量类型构成一个联合类型时,我们现在会规范化这些对象字面量类型,以确保联合类型的每个组成部分都包含所有属性。

¥TypeScript 2.7 improves type inference for multiple object literals occurring in the same context. When multiple object literal types contribute to a union type, we now normalize the object literal types such that all properties are present in each constituent of the union type.

考虑:

¥Consider:

ts
const obj = test ? { text: "hello" } : {}; // { text: string } | { text?: undefined }
const s = obj.text; // string | undefined

以前,obj 的类型被推断为 {},而第二行随后导致错误,因为 obj 似乎没有属性。这显然不太理想。

¥Previously type {} was inferred for obj and the second line subsequently caused an error because obj would appear to have no properties. That obviously wasn’t ideal.

示例

¥Example

ts
// let obj: { a: number, b: number } |
// { a: string, b?: undefined } |
// { a?: undefined, b?: undefined }
let obj = [{ a: 1, b: 2 }, { a: "abc" }, {}][0];
obj.a; // string | number | undefined
obj.b; // number | undefined

针对同一类型参数的多个对象字面量类型推断同样会被折叠成一个规范化的联合类型:

¥Multiple object literal type inferences for the same type parameter are similarly collapsed into a single normalized union type:

ts
declare function f<T>(...items: T[]): T;
// let obj: { a: number, b: number } |
// { a: string, b?: undefined } |
// { a?: undefined, b?: undefined }
let obj = f({ a: 1, b: 2 }, { a: "abc" }, {});
obj.a; // string | number | undefined
obj.b; // number | undefined

改进了对结构相同的类和 instanceof 表达式的处理

¥Improved handling of structurally identical classes and instanceof expressions

TypeScript 2.7 改进了联合类型和 instanceof 表达式中结构相同的类的处理:

¥TypeScript 2.7 improves the handling of structurally identical classes in union types and instanceof expressions:

  • 结构相同但不同的类类型现在保留在联合类型中(而不是只保留一个)。

    ¥Structurally identical, but distinct, class types are now preserved in union types (instead of eliminating all but one).

  • 联合类型子类型缩减仅当某个类类型是联合中另一个类类型的子类且派生自该类类型时才会删除该类类型。

    ¥Union type subtype reduction only removes a class type if it is a subclass of and derives from another class type in the union.

  • instanceof 运算符的类型检查现在基于左操作数的类型是否源自右操作数指示的类型(而不是结构子类型检查)。

    ¥Type checking of the instanceof operator is now based on whether the type of the left operand derives from the type indicated by the right operand (as opposed to a structural subtype check).

这意味着联合类型和 instanceof 能够正确区分结构相同的类。

¥This means that union types and instanceof properly distinguish between structurally identical classes.

示例

¥Example

ts
class A {}
class B extends A {}
class C extends A {}
class D extends A {
c: string;
}
class E extends D {}
let x1 = !true ? new A() : new B(); // A
let x2 = !true ? new B() : new C(); // B | C (previously B)
let x3 = !true ? new C() : new D(); // C | D (previously C)
let a1 = [new A(), new B(), new C(), new D(), new E()]; // A[]
let a2 = [new B(), new C(), new D(), new E()]; // (B | C | D)[] (previously B[])
function f1(x: B | C | D) {
if (x instanceof B) {
x; // B (previously B | D)
} else if (x instanceof C) {
x; // C
} else {
x; // D (previously never)
}
}

in 运算符推断出的类型保护

¥Type guards inferred from in operator

in 运算符现在充当类型的收缩表达式。

¥The in operator now acts as a narrowing expression for types.

对于 n in x 表达式,其中 n 是字符串字面量或字符串字面量类型,x 是联合类型,“true” 分支会缩小到具有可选或必需属性 n 的类型,“false” 分支会缩小到具有可选或缺失属性 n 的类型。

¥For a n in x expression, where n is a string literal or string literal type and x is a union type, the “true” branch narrows to types which have an optional or required property n, and the “false” branch narrows to types which have an optional or missing property n.

示例

¥Example

ts
interface A {
a: number;
}
interface B {
b: string;
}
function foo(x: A | B) {
if ("a" in x) {
return x.a;
}
return x.b;
}

支持从 CommonJS 模块和 --esModuleInterop 中调用 import d from "cjs"

¥Support for import d from "cjs" from CommonJS modules with --esModuleInterop

TypeScript 2.7 更新了 CommonJS/AMD/UMD 模块的触发,使其能够根据 esModuleInterop 下是否存在 __esModule 指示符来合成命名空间记录。此项更改使 TypeScript 生成的输出更接近 Babel 生成的输出。

¥TypeScript 2.7 updates CommonJS/AMD/UMD module emit to synthesize namespace records based on the presence of an __esModule indicator under esModuleInterop. The change brings the generated output from TypeScript closer to that generated by Babel.

以前,CommonJS/AMD/UMD 模块的处理方式与 ES6 模块相同,这导致了几个问题。即:

¥Previously CommonJS/AMD/UMD modules were treated in the same way as ES6 modules, resulting in a couple of problems. Namely:

  • TypeScript 将 CommonJS/AMD/UMD 模块的命名空间导入(即 import * as foo from "foo")视为与 const foo = require("foo") 等效。这里很简单,但如果导入的主要对象是原始类型、类或函数,则无法正常工作。ECMAScript 规范规定命名空间记录是一个普通对象,并且命名空间导入(上例中的 foo)不可调用,但 TypeScript 允许调用。

    ¥TypeScript treats a namespace import (i.e. import * as foo from "foo") for a CommonJS/AMD/UMD module as equivalent to const foo = require("foo").Things are simple here, but they don’t work out if the primary object being imported is a primitive or a class or a function. ECMAScript spec stipulates that a namespace record is a plain object, and that a namespace import (foo in the example above) is not callable, though allowed by TypeScript

  • 类似地,CommonJS/AMD/UMD 模块的默认导入(即 import d from "foo")等同于 const d = require("foo").default。目前大多数 CommonJS/AMD/UMD 模块都没有 default 导出,因此这种导入模式实际上无法用于导入非 ES 模块(即 CommonJS/AMD/UMD)。例如,不允许使用 import fs from "fs"import express from "express"

    ¥Similarly a default import (i.e. import d from "foo") for a CommonJS/AMD/UMD module as equivalent to const d = require("foo").default.Most of the CommonJS/AMD/UMD modules available today do not have a default export, making this import pattern practically unusable to import non-ES modules (i.e. CommonJS/AMD/UMD). For instance import fs from "fs" or import express from "express" are not allowed.

在新的 esModuleInterop 模式下,应该解决以下两个问题:

¥Under the new esModuleInterop these two issues should be addressed:

  • 命名空间导入(即 import * as foo from "foo")现在已正确标记为不可调用。调用它将导致错误。

    ¥A namespace import (i.e. import * as foo from "foo") is now correctly flagged as uncallable. Calling it will result in an error.

  • 现在允许默认导入 CommonJS/AMD/UMD(例如 import fs from "fs"),并且应该可以正常工作。

    ¥Default imports to CommonJS/AMD/UMD are now allowed (e.g. import fs from "fs"), and should work as expected.

注意:新行为通过标志添加,以避免对现有代码库造成不必要的破坏。我们强烈建议将其应用于新项目和现有项目。对于现有项目,命名空间导入 (import * as express from "express"; express();) 需要转换为默认导入 (import express from "express"; express();)。

¥Note: The new behavior is added under a flag to avoid unwarranted breaks to existing code bases. We highly recommend applying it both to new and existing projects. For existing projects, namespace imports (import * as express from "express"; express();) will need to be converted to default imports (import express from "express"; express();).

示例

¥Example

使用 esModuleInterop 类型,会生成两个新的辅助函数 __importStar__importDefault,分别用于导入 * 和导入 default。例如,输入如下内容:

¥With esModuleInterop two new helpers are generated __importStar and __importDefault for import * and import default respectively. For instance input like:

ts
import * as foo from "foo";
import b from "bar";

将生成:

¥Will generate:

js
"use strict";
var __importStar =
(this && this.__importStar) ||
function(mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null)
for (var k in mod)
if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
var __importDefault =
(this && this.__importDefault) ||
function(mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
exports.__esModule = true;
var foo = __importStar(require("foo"));
var bar_1 = __importDefault(require("bar"));

数字分隔符

¥Numeric separators

TypeScript 2.7 带来了对 ES 数字分隔符 的支持。现在可以使用 _ 将数字字面量拆分成多个段。

¥TypeScript 2.7 brings support for ES Numeric Separators. Numeric literals can now be separated into segments using _.

示例

¥Example

ts
const million = 1_000_000;
const phone = 555_734_2231;
const bytes = 0xff_0c_00_ff;
const word = 0b1100_0011_1101_0001;

--watch 模式下更清晰的输出

¥Cleaner output in --watch mode

TypeScript 的 --watch 模式现在会在请求重新编译后清除屏幕。

¥TypeScript’s --watch mode now clears the screen after a re-compilation is requested.

更美观的 --pretty 输出

¥Prettier --pretty output

TypeScript 的 pretty 标志可以使错误消息更易于阅读和管理。pretty 现在使用颜色来表示文件名、诊断代码和行号。文件名和位置现在也已格式化,以便在常用终端(例如 Visual Studio Code 终端)中导航。

¥TypeScript’s pretty flag can make error messages easier to read and manage. pretty now uses colors for file names, diagnostic codes, and line numbers. File names and positions are now also formatted to allow navigation in common terminals (e.g. Visual Studio Code terminal).