TypeScript 2.7
常量命名属性
TypeScript 2.7 增加了在类型上声明常量命名属性的支持,包括 ECMAScript 符号。
示例
// 库
export const SERIALIZE = Symbol("serialize-method-key");
export interface Serializable {
[SERIALIZE](obj: {}): string;
}// 使用者
import { SERIALIZE, Serializable } from "lib";
class JSONSerializableItem implements Serializable {
[SERIALIZE](obj: {}) {
return JSON.stringify(obj);
}
}这也适用于数字和字符串字面量。
示例
const Foo = "Foo";
const Bar = "Bar";
let x = {
[Foo]: 100,
[Bar]: "hello"
};
let a = x[Foo]; // 类型为 'number'
let b = x[Bar]; // 类型为 'string'unique symbol
为了能够将符号视为唯一的字面量,可以使用一种新的类型 unique symbol。 unique symbol 是 symbol 的子类型,仅通过调用 Symbol() 或 Symbol.for() 或通过显式类型注解产生。 这种新类型仅允许用于 const 声明和 readonly static 属性,并且要引用特定的唯一符号,你必须使用 typeof 运算符。 每个对 unique symbol 的引用都意味着一个完全独立的身份,该身份与给定的声明绑定。
示例
// 有效
declare const Foo: unique symbol;
// 错误!'Bar' 不是常量。
let Bar: unique symbol = Symbol();
// 有效 - 引用了一个唯一符号,但其身份与 'Foo' 绑定。
let Baz: typeof Foo = Foo;
// 也有效。
class C {
static readonly StaticSymbol: unique symbol = Symbol();
}由于每个 unique symbol 都有一个完全独立的身份,因此没有两个 unique symbol 类型可以相互赋值或比较。
示例
const Foo = Symbol();
const Bar = Symbol();
// 错误:不能比较两个唯一符号。
if (Foo === Bar) {
// ...
}严格类初始化
TypeScript 2.7 引入了一个名为 strictPropertyInitialization 的新标志。 此标志执行检查,以确保类的每个实例属性在构造函数体中或通过属性初始化器被初始化。 例如
class C {
foo: number;
bar = "hello";
baz: boolean;
// ~~~
// 错误!属性 'baz' 没有初始化器,并且在构造函数中未被明确赋值。
constructor() {
this.foo = 42;
}
}在上面的例子中,如果我们确实打算让 baz 可能为 undefined,我们应该将其声明为 boolean | undefined 类型。
在某些情况下,属性可以被间接初始化(可能通过辅助方法或依赖注入库),这时你可以对你的属性使用新的明确赋值断言修饰符(下面讨论)。
class C {
foo!: number;
// ^
// 注意这个 '!' 修饰符。
// 这就是“明确赋值断言”
constructor() {
this.initialize();
}
initialize() {
this.foo = 0;
}
}请记住,strictPropertyInitialization 将与其他 strict 模式标志一起开启,这可能会影响你的项目。 你可以在 tsconfig.json 的 compilerOptions 中将 strictPropertyInitialization 设置为 false,或者在命令行中使用 --strictPropertyInitialization false 来关闭此检查。
明确赋值断言
明确赋值断言是一个特性,允许在实例属性和变量声明后放置一个 !,以向 TypeScript 传达该变量确实已被赋值,即使 TypeScript 的分析无法检测到。
示例
let x: number;
initialize();
console.log(x + x);
// ~ ~
// 错误!变量 'x' 在赋值前被使用。
function initialize() {
x = 10;
}使用明确赋值断言,我们可以通过在声明后附加一个 ! 来断言 x 确实被赋值了:
// 注意这个 '!'
let x!: number;
initialize();
// 没有错误!
console.log(x + x);
function initialize() {
x = 10;
}在某种意义上,明确赋值断言运算符是非空断言运算符(表达式后跟 !)的对偶,我们也可以在示例中使用它。
let x: number;
initialize();
// 没有错误!
console.log(x! + x!);
function initialize() {
x = 10;
}在我们的示例中,我们知道 x 的所有使用都会被初始化,因此使用明确赋值断言比使用非空断言更有意义。
固定长度元组
在 TypeScript 2.6 及更早版本中,[number, string, string] 被认为是 [number, string] 的子类型。 这是由 TypeScript 的结构性本质驱动的;[number, string, string] 的第一和第二个元素分别是 [number, string] 的第一和第二个元素的子类型。 然而,在检查了元组的实际使用情况后,我们注意到大多数允许这种情况的场景通常都是不希望的。
在 TypeScript 2.7 中,不同长度的元组不再能够相互赋值。 感谢 Kiara Grouwstra 的拉取请求,元组类型现在将其长度编码到各自 length 属性的类型中。 这是通过利用数字字面量类型实现的,现在允许元组与不同长度的元组区分开来。
从概念上讲,你可以认为类型 [number, string] 等价于以下 NumStrTuple 的声明:
interface NumStrTuple extends Array<number | string> {
0: number;
1: string;
length: 2; // 使用数字字面量类型 '2'
}请注意,这对某些代码来说是一个破坏性更改。 如果你需要恢复到元组仅强制执行最小长度的原始行为,你可以使用一个类似的声明,不显式定义 length 属性,回退到 number。
interface MinimumNumStrTuple extends Array<number | string> {
0: number;
1: string;
}请注意,这并不意味着元组表示不可变数组,但它是一种隐含的约定。
对象字面量的改进类型推断
TypeScript 2.7 改进了在同一上下文中出现的多个对象字面量的类型推断。 当多个对象字面量类型贡献给一个联合类型时,我们现在规范化对象字面量类型,使得所有属性都出现在联合类型的每个组成部分中。
考虑:
const obj = test ? { text: "hello" } : {}; // { text: string } | { text?: undefined }
const s = obj.text; // string | undefined以前,为 obj 推断出类型 {},并且第二行随后导致错误,因为 obj 似乎没有属性。这显然不理想。
示例
// 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对于同一个类型参数的多个对象字面量类型推断,也会类似地折叠成一个单一的标准联合类型:
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 表达式的处理
TypeScript 2.7 改进了联合类型和 instanceof 表达式中结构相同类的处理:
- 结构相同但不同的类类型现在保留在联合类型中(而不是消除所有但保留一个)。
- 联合类型子类型缩减仅在某个类类型是联合中另一个类类型的子类并且派生自它时才移除该类类型。
instanceof运算符的类型检查现在基于左操作数的类型是否派生自右操作数指示的类型(而不是结构子类型检查)。
这意味着联合类型和 instanceof 可以正确区分结构相同的类。
示例
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 (之前是 B)
let x3 = !true ? new C() : new D(); // C | D (之前是 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)[] (之前是 B[])
function f1(x: B | C | D) {
if (x instanceof B) {
x; // B (之前是 B | D)
} else if (x instanceof C) {
x; // C
} else {
x; // D (之前是 never)
}
}从 in 运算符推断出的类型守卫
in 运算符现在充当类型的收窄表达式。
对于 n in x 表达式,其中 n 是字符串字面量或字符串字面量类型,且 x 是联合类型,则 "true" 分支收窄为具有可选或必需属性 n 的类型,而 "false" 分支收窄为具有可选或缺失属性 n 的类型。
示例
interface A {
a: number;
}
interface B {
b: string;
}
function foo(x: A | B) {
if ("a" in x) {
return x.a;
}
return x.b;
}在 --esModuleInterop 下从 CommonJS 模块支持 import d from "cjs"
TypeScript 2.7 更新了 CommonJS/AMD/UMD 模块输出,以在 esModuleInterop 下基于 __esModule 指示符的存在合成命名空间记录。 此更改使 TypeScript 生成的输出更接近 Babel 生成的输出。
以前,CommonJS/AMD/UMD 模块被视为与 ES6 模块相同的方式,导致了一些问题。即:
- TypeScript 将 CommonJS/AMD/UMD 模块的命名空间导入(即
import * as foo from "foo")视为等同于const foo = require("foo")。这里很简单,但如果被导入的主要对象是原始类型、类或函数,则不起作用。ECMAScript 规范规定命名空间记录是一个普通对象,并且命名空间导入(上面的示例中的foo)不可调用,尽管 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"。
在新的 esModuleInterop 下,这两个问题应该得到解决:
- 命名空间导入(即
import * as foo from "foo")现在被正确地标记为不可调用。调用它将导致错误。 - 现在允许对 CommonJS/AMD/UMD 模块进行默认导入(例如
import fs from "fs"),并且应该按预期工作。
注意:新行为是在一个标志下添加的,以避免对现有代码库造成不必要的破坏。我们强烈建议将其应用于新项目和现有项目。 对于现有项目,命名空间导入(
import * as express from "express"; express();)需要转换为默认导入(import express from "express"; express();)。
示例
在 esModuleInterop 下,分别为导入 * 和导入 default 生成了两个新的辅助函数 __importStar 和 __importDefault。 例如输入:
import * as foo from "foo";
import b from "bar";将生成:
"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"));数字分隔符
TypeScript 2.7 带来了对 ES 数字分隔符的支持。 数字字面量现在可以使用 _ 分隔成多个部分。
示例
const million = 1_000_000;
const phone = 555_734_2231;
const bytes = 0xff_0c_00_ff;
const word = 0b1100_0011_1101_0001;--watch 模式下更清晰的输出
TypeScript 的 --watch 模式现在会在请求重新编译后清除屏幕。
更美观的 --pretty 输出
TypeScript 的 pretty 标志可以使错误消息更易于阅读和管理。 pretty 现在对文件名、诊断代码和行号使用颜色。 文件名和位置现在也被格式化,以便在常见终端(例如 Visual Studio Code 终端)中进行导航。