TypeScript 2.1

keyof 和查找类型

¥keyof and Lookup Types

在 JavaScript 中,API 需要属性名作为参数的情况相当普遍,但目前还无法表达这些 API 中出现的类型关系。

¥In JavaScript it is fairly common to have APIs that expect property names as parameters, but so far it hasn’t been possible to express the type relationships that occur in those APIs.

输入索引类型查询或 keyof;索引类型查询 keyof T 可得出 T 允许的属性名称类型。keyof T 类型被视为 string 的子类型。

¥Enter Index Type Query or keyof; An indexed type query keyof T yields the type of permitted property names for T. A keyof T type is considered a subtype of string.

示例

¥Example

ts
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string

与之对应的是索引访问类型,也称为查找类型。从语法上讲,它们看起来与元素访问完全相同,但写成类型:

¥The dual of this is indexed access types, also called lookup types. Syntactically, they look exactly like an element access, but are written as types:

示例

¥Example

ts
type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string

你可以将此模式与类型系统的其他部分一起使用,以获得类型安全的查找。

¥You can use this pattern with other parts of the type system to get type-safe lookups.

ts
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // Inferred type is T[K]
}
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
let x = { foo: 10, bar: "hello!" };
let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string
let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"
setProperty(x, "foo", "string"); // Error!, string expected number

映射类型

¥Mapped Types

一项常见的任务是采用现有类型并使其每个属性完全可选。假设我们有一个 Person

¥One common task is to take an existing type and make each of its properties entirely optional. Let’s say we have a Person:

ts
interface Person {
name: string;
age: number;
location: string;
}

部分版本如下:

¥A partial version of it would be:

ts
interface PartialPerson {
name?: string;
age?: number;
location?: string;
}

使用映射类型,PartialPerson 可以写成对类型 Person 的广义转换,如下所示:

¥with Mapped types, PartialPerson can be written as a generalized transformation on the type Person as:

ts
type Partial<T> = {
[P in keyof T]?: T[P];
};
type PartialPerson = Partial<Person>;

映射类型是通过对字面量类型进行并集运算,并计算出一组新对象类型的属性而生成的。它们类似于 Python 中的列表推导式,但不是在列表中生成新元素,而是在类型中生成新属性。

¥Mapped types are produced by taking a union of literal types, and computing a set of properties for a new object type. They’re like list comprehensions in Python, but instead of producing new elements in a list, they produce new properties in a type.

除了 Partial 之外,映射类型还可以对类型进行许多有用的转换:

¥In addition to Partial, Mapped Types can express many useful transformations on types:

ts
// Keep types the same, but make each property to be read-only.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Same property names, but make the value a promise instead of a concrete one
type Deferred<T> = {
[P in keyof T]: Promise<T[P]>;
};
// Wrap proxies around properties of T
type Proxify<T> = {
[P in keyof T]: { get(): T[P]; set(v: T[P]): void };
};

PartialReadonlyRecordPick

¥Partial, Readonly, Record, and Pick

如前所述,PartialReadonly 是非常有用的构造函数。你可以使用它们来描述一些常见的 JS 例程,例如:

¥Partial and Readonly, as described earlier, are very useful constructs. You can use them to describe some common JS routines like:

ts
function assign<T>(obj: T, props: Partial<T>): void;
function freeze<T>(obj: T): Readonly<T>;

因此,它们现在默认包含在标准库中。

¥Because of that, they are now included by default in the standard library.

我们还包含另外两种工具类型:RecordPick

¥We’re also including two other utility types as well: Record and Pick.

ts
// From T pick a set of properties K
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
ts
// For every properties K of type T, transform it to U
function mapObject<K extends string, T, U>(
obj: Record<K, T>,
f: (x: T) => U
): Record<K, U>;
const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }

对象扩展和剩余操作

¥Object Spread and Rest

TypeScript 2.1 引入了对 ESnext 扩展和剩余 的支持。

¥TypeScript 2.1 brings support for ESnext Spread and Rest.

与数组展开类似,展开对象可以方便地获取浅拷贝:

¥Similar to array spread, spreading an object can be handy to get a shallow copy:

ts
let copy = { ...original };

类似地,你可以合并多个不同的对象。在下面的例子中,merged 将具有来自 foobarbaz 的属性。

¥Similarly, you can merge several different objects. In the following example, merged will have properties from foo, bar, and baz.

ts
let merged = { ...foo, ...bar, ...baz };

你还可以覆盖现有属性并添加新属性:

¥You can also override existing properties and add new ones:

ts
let obj = { x: 1, y: "string" };
var newObj = { ...obj, z: 3, y: 4 }; // { x: number, y: number, z: number }

指定扩展操作的顺序决定了最终结果对象中的属性;稍后的属性将 “胜出” 扩展到先前创建的属性上。

¥The order of specifying spread operations determines what properties end up in the resulting object; properties in later spreads “win out” over previously created properties.

对象剩余表达式是对象展开的对偶,因为它们可以提取在元素解构时未拾取的任何额外属性:

¥Object rests are the dual of object spreads, in that they can extract any extra properties that don’t get picked up when destructuring an element:

ts
let obj = { x: 1, y: 1, z: 1 };
let { z, ...obj1 } = obj;
obj1; // {x: number, y:number};

下级异步函数

¥Downlevel Async Functions

此功能在 TypeScript 2.1 之前就已支持,但仅限于针对 ES6/ES2015 的版本。TypeScript 2.1 将该功能引入 ES3 和 ES5 运行时,这意味着无论你使用什么环境,都可以自由地利用它。

¥This feature was supported before TypeScript 2.1, but only when targeting ES6/ES2015. TypeScript 2.1 brings the capability to ES3 and ES5 run-times, meaning you’ll be free to take advantage of it no matter what environment you’re using.

注意:首先,我们需要确保我们的运行时全局拥有符合 ECMAScript 标准的 Promise。这可能涉及为 Promise 获取 一个 polyfill,或者依赖于你在目标运行时可能拥有的 一个 polyfill。我们还需要通过将 lib 选项设置为 "dom", "es2015""dom", "es2015.promise", "es5" 之类的值来确保 TypeScript 知道 Promise 的存在。

¥Note: first, we need to make sure our run-time has an ECMAScript-compliant Promise available globally. That might involve grabbing a polyfill for Promise, or relying on one that you might have in the run-time that you’re targeting. We also need to make sure that TypeScript knows Promise exists by setting your lib option to something like "dom", "es2015" or "dom", "es2015.promise", "es5"

示例

¥Example

tsconfig.json
{
"": ["dom", "es2015.promise", "es5"]
}
}

dramaticWelcome.ts
ts
function delay(milliseconds: number) {
return new Promise<void>(resolve => {
setTimeout(resolve, milliseconds);
});
}
async function dramaticWelcome() {
console.log("Hello");
for (let i = 0; i < 3; i++) {
await delay(500);
console.log(".");
}
console.log("World!");
}
dramaticWelcome();

在 ES3/ES5 引擎上编译和运行输出应该能够得到正确的结果。

¥Compiling and running the output should result in the correct behavior on an ES3/ES5 engine.

支持外部辅助库 (tslib)

¥Support for external helpers library (tslib)

TypeScript 注入了一些辅助函数,例如用于继承的 __extends、用于对象字面量和 JSX 元素中展开运算符的 __assign 以及用于异步函数的 __awaiter

¥TypeScript injects a handful of helper functions such as __extends for inheritance, __assign for spread operator in object literals and JSX elements, and __awaiter for async functions.

之前有两个选项:

¥Previously there were two options:

  1. 在每个需要它们的文件中注入辅助函数,或者

    ¥inject helpers in every file that needs them, or

  2. noEmitHelpers 完全不需要任何辅助函数。

    ¥no helpers at all with noEmitHelpers.

这两个选项还有待改进;对于试图保持包大小较小的客户来说,将辅助函数打包在每个文件中是一个痛点。不包含辅助函数意味着客户必须维护自己的辅助函数库。

¥The two options left more to be desired; bundling the helpers in every file was a pain point for customers trying to keep their package size small. And not including helpers, meant customers had to maintain their own helpers library.

TypeScript 2.1 允许将这些文件一次性地添加到项目中的单独模块中,编译器将根据需要向这些文件触发导入操作。

¥TypeScript 2.1 allows for including these files in your project once in a separate module, and the compiler will emit imports to them as needed.

首先,安装 tslib 工具库:

¥First, install the tslib utility library:

sh
npm install tslib

其次,使用 importHelpers 编译你的文件:

¥Second, compile your files using importHelpers:

sh
tsc --module commonjs --importHelpers a.ts

因此,给定以下输入,生成的 .js 文件将包含对 tslib 的导入,并使用其中的 __assign 辅助函数,而不是内联它。

¥So given the following input, the resulting .js file will include an import to tslib and use the __assign helper from it instead of inlining it.

ts
export const o = { a: 1, name: "o" };
export const copy = { ...o };
js
"use strict";
var tslib_1 = require("tslib");
exports.o = { a: 1, name: "o" };
exports.copy = tslib_1.__assign({}, exports.o);

无类型导入

¥Untyped imports

TypeScript 历来对模块的导入方式要求过于严格。这是为了避免拼写错误并防止用户错误使用模块。

¥TypeScript has traditionally been overly strict about how you can import modules. This was to avoid typos and prevent users from using modules incorrectly.

然而,很多时候,你可能只想导入一个现有的模块,而这个模块可能没有自己的 .d.ts 文件。以前,这是一个错误。从 TypeScript 2.1 开始,现在更容易了。

¥However, a lot of the time, you might just want to import an existing module that may not have its own .d.ts file. Previously this was an error. Starting with TypeScript 2.1 this is now much easier.

在 TypeScript 2.1 中,你可以导入 JavaScript 模块而无需类型声明。如果存在类型声明(例如 declare module "foo" { ... }node_modules/@types/foo),则它仍然优先。

¥With TypeScript 2.1, you can import a JavaScript module without needing a type declaration. A type declaration (such as declare module "foo" { ... } or node_modules/@types/foo) still takes priority if it exists.

noImplicitAny 下,导入到没有声明文件的模块仍会被标记为错误。

¥An import to a module with no declaration file will still be flagged as an error under noImplicitAny.

示例

¥Example

ts
// Succeeds if `node_modules/asdf/index.js` exists
import { x } from "asdf";

支持 --target ES2016--target ES2017--target ESNext

¥Support for --target ES2016, --target ES2017 and --target ESNext

TypeScript 2.1 支持三个新的目标值:--target ES2016--target ES2017--target ESNext

¥TypeScript 2.1 supports three new target values --target ES2016, --target ES2017 and --target ESNext.

使用目标 --target ES2016 将指示编译器不转换 ES2016 特有的功能,例如 ** 运算符。

¥Using target --target ES2016 will instruct the compiler not to transform ES2016-specific features, e.g. ** operator.

同样,--target ES2017 将指示编译器不要转换 ES2017 特有的功能,例如 async/await

¥Similarly, --target ES2017 will instruct the compiler not to transform ES2017-specific features like async/await.

--target ESNext 的目标是最新支持的 ES 提议的功能

¥--target ESNext targets latest supported ES proposed features.

改进了 any 推断

¥Improved any Inference

以前,如果 TypeScript 无法确定变量,它会选择 any 类型。

¥Previously, if TypeScript couldn’t figure out the type of a variable, it would choose the any type.

ts
let x; // implicitly 'any'
let y = []; // implicitly 'any[]'
let z: any; // explicitly 'any'.

在 TypeScript 2.1 中,TypeScript 不会仅仅选择 any,而是会根据你最终赋值的内容推断类型。

¥With TypeScript 2.1, instead of just choosing any, TypeScript will infer types based on what you end up assigning later on.

仅当设置了 noImplicitAny 时才启用此功能。

¥This is only enabled if noImplicitAny is set.

示例

¥Example

ts
let x;
// You can still assign anything you want to 'x'.
x = () => 42;
// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.
let y = x();
// Thanks to that, it will now tell you that you can't add a number to a function!
console.log(x + y);
// ~~~~~
// Error! Operator '+' cannot be applied to types '() => number' and 'number'.
// TypeScript still allows you to assign anything you want to 'x'.
x = "Hello world!";
// But now it also knows that 'x' is a 'string'!
x.toLowerCase();

现在也对空数组进行同样的跟踪。

¥The same sort of tracking is now also done for empty arrays.

声明时未使用类型注释且初始值为 [] 的变量被视为隐式 any[] 变量。然而,每个后续的 x.push(value)x.unshift(value)x[n] = value 操作都会根据添加到变量中的元素来演变变量的类型。

¥A variable declared with no type annotation and an initial value of [] is considered an implicit any[] variable. However, each subsequent x.push(value), x.unshift(value) or x[n] = value operation evolves the type of the variable in accordance with what elements are added to it.

ts
function f1() {
let x = [];
x.push(5);
x[1] = "hello";
x.unshift(true);
return x; // (string | number | boolean)[]
}
function f2() {
let x = null;
if (cond()) {
x = [];
while (cond()) {
x.push("hello");
}
}
return x; // string[] | null
}

隐式 any 错误

¥Implicit any errors

这样做的一大好处是,在使用 noImplicitAny 运行时,你将看到更少的隐式 any 错误。仅当编译器无法在没有类型注释的情况下识别变量的类型时,才会报告隐式 any 错误。

¥One great benefit of this is that you’ll see way fewer implicit any errors when running with noImplicitAny. Implicit any errors are only reported when the compiler is unable to know the type of a variable without a type annotation.

示例

¥Example

ts
function f3() {
let x = []; // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined.
x.push(5);
function g() {
x; // Error: Variable 'x' implicitly has an 'any[]' type.
}
}

更好地推断字面量类型

¥Better inference for literal types

字符串、数字和布尔字面量类型(例如 "abc"1true)以前仅在存在显式类型注释的情况下推断。从 TypeScript 2.1 开始,始终推断 const 变量和 readonly 属性的字面类型。

¥String, numeric and boolean literal types (e.g. "abc", 1, and true) were previously inferred only in the presence of an explicit type annotation. Starting with TypeScript 2.1, literal types are always inferred for const variables and readonly properties.

对于没有类型注释的 const 变量或 readonly 属性,推断的类型是字面量初始化器的类型。对于有初始化器但没有类型注释的 let 变量、var 变量、参数或非 readonly 属性,推断的类型是初始化器的扩展字面量类型。其中,字符串字面量类型的扩展类型为 string,数字字面量类型的扩展类型为 numbertruefalse 的扩展类型为 boolean,枚举字面量类型的包含枚举类型为 XX。

¥The type inferred for a const variable or readonly property without a type annotation is the type of the literal initializer. The type inferred for a let variable, var variable, parameter, or non-readonly property with an initializer and no type annotation is the widened literal type of the initializer. Where the widened type for a string literal type is string, number for numeric literal types, boolean for true or false and the containing enum for enum literal types.

示例

¥Example

ts
const c1 = 1; // Type 1
const c2 = c1; // Type 1
const c3 = "abc"; // Type "abc"
const c4 = true; // Type true
const c5 = cond ? 1 : "abc"; // Type 1 | "abc"
let v1 = 1; // Type number
let v2 = c2; // Type number
let v3 = c3; // Type string
let v4 = c4; // Type boolean
let v5 = c5; // Type number | string

可以通过显式类型注释来控制字面量类型扩展。具体来说,当在没有类型注解的情况下推断 const 位置的字面量类型表达式时,该 const 变量会被推断为扩展字面量类型。但是,当 const 位置具有显式字面量类型注解时,const 变量将获得非扩展字面量类型。

¥Literal type widening can be controlled through explicit type annotations. Specifically, when an expression of a literal type is inferred for a const location without a type annotation, that const variable gets a widening literal type inferred. But when a const location has an explicit literal type annotation, the const variable gets a non-widening literal type.

示例

¥Example

ts
const c1 = "hello"; // Widening type "hello"
let v1 = c1; // Type string
const c2: "hello" = "hello"; // Type "hello"
let v2 = c2; // Type "hello"

将 super 调用的返回值用作 ‘this’

¥Use returned values from super calls as ‘this’

在 ES2015 中,返回对象的构造函数隐式地将 this 的值替换为 super() 的任何调用者。因此,有必要捕获 super() 的任何潜在返回值并将其替换为 this。此更改支持与 自定义元素 配合使用,自定义元素 利用此功能使用用户编写的构造函数来初始化浏览器分配的元素。

¥In ES2015, constructors which return an object implicitly substitute the value of this for any callers of super(). As a result, it is necessary to capture any potential return value of super() and replace it with this. This change enables working with Custom Elements, which takes advantage of this to initialize browser-allocated elements with user-written constructors.

示例

¥Example

ts
class Base {
x: number;
constructor() {
// return a new object other than `this`
return {
x: 1
};
}
}
class Derived extends Base {
constructor() {
super();
this.x = 2;
}
}

生成:

¥Generates:

js
var Derived = (function(_super) {
__extends(Derived, _super);
function Derived() {
var _this = _super.call(this) || this;
_this.x = 2;
return _this;
}
return Derived;
})(Base);

此更改会导致扩展内置类(例如 ErrorArrayMap 等)的行为发生中断。有关更多详细信息,请参阅 扩展内置函数的重大变更文档

¥This change entails a break in the behavior of extending built-in classes like Error, Array, Map, etc.. Please see the extending built-ins breaking change documentation for more details.

配置继承

¥Configuration inheritance

一个项目通常有多个输出目标,例如 ES5ES2015、调试和生产,或者 CommonJSSystem;这两个目标之间只需更改几个配置选项,维护多个 tsconfig.json 文件可能会很麻烦。

¥Often a project has multiple output targets, e.g. ES5 and ES2015, debug and production or CommonJS and System; Just a few configuration options change between these two targets, and maintaining multiple tsconfig.json files can be a hassle.

TypeScript 2.1 支持使用 extends 继承配置,其中:

¥TypeScript 2.1 supports inheriting configuration using extends, where:

  • extendstsconfig.json 中的新顶层属性(与 compilerOptionsfilesincludeexclude 并列)。

    ¥extends is a new top-level property in tsconfig.json (alongside compilerOptions, files, include, and exclude).

  • extends 的值必须是包含要继承自的另一个配置文件路径的字符串。

    ¥The value of extends must be a string containing a path to another configuration file to inherit from.

  • 首先加载基础文件中的配置,然后由继承配置文件中的配置覆盖。

    ¥The configuration from the base file is loaded first, then overridden by those in the inheriting config file.

  • 配置文件之间不允许循环。

    ¥Circularity between configuration files is not allowed.

  • 继承配置文件中的 filesincludeexclude 将覆盖基础配置文件中的 filesincludeexclude

    ¥files, include, and exclude from the inheriting config file overwrite those from the base config file.

  • 配置文件中找到的所有相对路径都将相对于它们起源的配置文件进行解析。

    ¥All relative paths found in the configuration file will be resolved relative to the configuration file they originated in.

示例

¥Example

configs/base.json

tsconfig.json

{
"": "./configs/base",
"": ["main.ts", "supplemental.ts"]
}

tsconfig.nostrictnull.json

{
"": "./tsconfig",
}
}

新的 --alwaysStrict

¥New --alwaysStrict

使用 alwaysStrict 调用编译器会导致:

¥Invoking the compiler with alwaysStrict causes:

  1. 以严格模式解析所有代码。

    ¥Parses all the code in strict mode.

  2. 在每个生成的文件顶部写入 "use strict"; 指令。

    ¥Writes "use strict"; directive atop every generated file.

模块在严格模式下自动解析。建议在非模块代码中使用新标志。

¥Modules are parsed automatically in strict mode. The new flag is recommended for non-module code.