TypeScript 2.9

使用 keyof 和映射类型支持 numbersymbol 命名属性

¥Support number and symbol named properties with keyof and mapped types

TypeScript 2.9 添加了对索引类型和映射类型中 numbersymbol 命名属性的支持。以前,keyof 运算符和映射类型仅支持 string 命名属性。

¥TypeScript 2.9 adds support for number and symbol named properties in index types and mapped types. Previously, the keyof operator and mapped types only supported string named properties.

更改包括:

¥Changes include:

  • 某些类型 T 的索引类型 keyof Tstring | number | symbol 的子类型。

    ¥An index type keyof T for some type T is a subtype of string | number | symbol.

  • 映射类型 { [P in K]: XXX } 允许任何 K 赋值给 string | number | symbol

    ¥A mapped type { [P in K]: XXX } permits any K assignable to string | number | symbol.

  • 在泛型对象的 for...in 语句中 T,迭代变量的推断类型之前为 keyof T,但现在为 Extract<keyof T, string>。(换句话说,keyof T 的子集只包含类似字符串的值。)

    ¥In a for...in statement for an object of a generic type T, the inferred type of the iteration variable was previously keyof T but is now Extract<keyof T, string>. (In other words, the subset of keyof T that includes only string-like values.)

给定一个对象类型 Xkeyof X 的解析如下:

¥Given an object type X, keyof X is resolved as follows:

  • 如果 X 包含字符串索引签名,则 keyof Xstringnumber 和表示符号属性的字面类型的并集;否则

    ¥If X contains a string index signature, keyof X is a union of string, number, and the literal types representing symbol-like properties, otherwise

  • 如果 X 包含数字索引签名,则 keyof Xnumber 与表示字符串和符号属性的字面类型的并集;否则

    ¥If X contains a numeric index signature, keyof X is a union of number and the literal types representing string-like and symbol-like properties, otherwise

  • keyof X 是表示字符串、数字和符号属性的字面量类型的联合。

    ¥keyof X is a union of the literal types representing string-like, number-like, and symbol-like properties.

其中:

¥Where:

  • 对象类型的类似字符串的属性是使用标识符、字符串字面量或字符串字面量类型的计算属性名称声明的属性。

    ¥String-like properties of an object type are those declared using an identifier, a string literal, or a computed property name of a string literal type.

  • 对象类型的类似数字的属性是使用数字字面量或数字字面量类型的计算属性名称声明的属性。

    ¥Number-like properties of an object type are those declared using a numeric literal or computed property name of a numeric literal type.

  • 对象类型的类符号属性是使用唯一符号类型的计算属性名称声明的属性。

    ¥Symbol-like properties of an object type are those declared using a computed property name of a unique symbol type.

在映射类型 { [P in K]: XXX } 中,K 中的每个字符串字面量类型都会引入一个具有字符串名称的属性,K 中的每个数字字面量类型都会引入一个具有数字名称的属性,K 中的每个唯一符号类型都会引入一个具有唯一符号名称的属性。此外,如果 K 包含类型 string,则引入字符串索引签名;如果 K 包含类型 number,则引入数字索引签名。

¥In a mapped type { [P in K]: XXX }, each string literal type in K introduces a property with a string name, each numeric literal type in K introduces a property with a numeric name, and each unique symbol type in K introduces a property with a unique symbol name. Furthermore, if K includes type string, a string index signature is introduced, and if K includes type number, a numeric index signature is introduced.

示例

¥Example

ts
const c = "c";
const d = 10;
const e = Symbol();
const enum E1 {
A,
B,
C,
}
const enum E2 {
A = "A",
B = "B",
C = "C",
}
type Foo = {
a: string; // String-like name
5: string; // Number-like name
[c]: string; // String-like name
[d]: string; // Number-like name
[e]: string; // Symbol-like name
[E1.A]: string; // Number-like name
[E2.A]: string; // String-like name
};
type K1 = keyof Foo; // "a" | 5 | "c" | 10 | typeof e | E1.A | E2.A
type K2 = Extract<keyof Foo, string>; // "a" | "c" | E2.A
type K3 = Extract<keyof Foo, number>; // 5 | 10 | E1.A
type K4 = Extract<keyof Foo, symbol>; // typeof e

由于 keyof 现在通过在键类型中包含类型 number 来反映数字索引签名的存在,因此映射类型(例如 Partial<T>Readonly<T>)在应用于具有数字索引签名的对象类型时可以正常工作:

¥Since keyof now reflects the presence of a numeric index signature by including type number in the key type, mapped types such as Partial<T> and Readonly<T> work correctly when applied to object types with numeric index signatures:

ts
type Arrayish<T> = {
length: number;
[x: number]: T;
};
type ReadonlyArrayish<T> = Readonly<Arrayish<T>>;
declare const map: ReadonlyArrayish<string>;
let n = map.length;
let x = map[123]; // Previously of type any (or an error with --noImplicitAny)

此外,由于 keyof 运算符支持 numbersymbol 命名键,现在可以抽象访问通过数字字面量(例如数字枚举类型)和唯一符号索引的对象属性。

¥Furthermore, with the keyof operator’s support for number and symbol named keys, it is now possible to abstract over access to properties of objects that are indexed by numeric literals (such as numeric enum types) and unique symbols.

ts
const enum Enum {
A,
B,
C,
}
const enumToStringMap = {
[Enum.A]: "Name A",
[Enum.B]: "Name B",
[Enum.C]: "Name C",
};
const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();
const symbolToNumberMap = {
[sym1]: 1,
[sym2]: 2,
[sym3]: 3,
};
type KE = keyof typeof enumToStringMap; // Enum (i.e. Enum.A | Enum.B | Enum.C)
type KS = keyof typeof symbolToNumberMap; // typeof sym1 | typeof sym2 | typeof sym3
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let x1 = getValue(enumToStringMap, Enum.C); // Returns "Name C"
let x2 = getValue(symbolToNumberMap, sym3); // Returns 3

这是一个重大变更;以前,keyof 运算符和映射类型仅支持 string 命名属性。如果代码中将 keyof T 类型的值始终假定为 string,现在将被标记为错误。

¥This is a breaking change; previously, the keyof operator and mapped types only supported string named properties. Code that assumed values typed with keyof T were always strings, will now be flagged as error.

示例

¥Example

ts
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string = k; // Error: keyof T is not assignable to string
}

建议

¥Recommendations

  • 如果你的函数只能处理以字符串命名的属性键,请在声明中使用 Extract<keyof T, string>

    ¥If your functions are only able to handle string named property keys, use Extract<keyof T, string> in the declaration:

    ts
    function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
    var name: string = k; // OK
    }
  • 如果你的函数可以处理所有属性键,则更改应在下游进行:

    ¥If your functions are open to handling all property keys, then the changes should be done down-stream:

    ts
    function useKey<T, K extends keyof T>(o: T, k: K) {
    var name: string | number | symbol = k;
    }
  • 否则,使用 keyofStringsOnly 编译器选项禁用新行为。

    ¥Otherwise use keyofStringsOnly compiler option to disable the new behavior.

JSX 元素中的泛型类型参数

¥Generic type arguments in JSX elements

JSX 元素现在允许将类型参数传递给泛型组件。

¥JSX elements now allow passing type arguments to generic components.

示例

¥Example

ts
class GenericComponent<P> extends React.Component<P> {
internalProp: P;
}
type Props = { a: number; b: string };
const x = <GenericComponent<Props> a={10} b="hi" />; // OK
const y = <GenericComponent<Props> a={10} b={20} />; // Error

泛型标记模板中的泛型类型参数

¥Generic type arguments in generic tagged templates

标记模板是 ECMAScript 2015 中引入的一种调用形式。与调用表达式类似,泛型函数可以在带标签的模板中使用,TypeScript 会推断所使用的参数类型。

¥Tagged templates are a form of invocation introduced in ECMAScript 2015. Like call expressions, generic functions may be used in a tagged template and TypeScript will infer the type arguments utilized.

TypeScript 2.9 允许将泛型类型参数传递给带标签的模板字符串。

¥TypeScript 2.9 allows passing generic type arguments to tagged template strings.

示例

¥Example

ts
declare function styledComponent<Props>(
strs: TemplateStringsArray
): Component<Props>;
interface MyProps {
name: string;
age: number;
}
styledComponent<MyProps>`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
declare function tag<T>(strs: TemplateStringsArray, ...args: T[]): T;
// inference fails because 'number' and 'string' are both candidates that conflict
let a = tag<string | number>`${100} ${"hello"}`;

import 类型

¥import types

模块可以导入在其他模块中声明的类型。但非模块全局脚本无法访问模块中声明的类型。输入 import 个类型。

¥Modules can import types declared in other modules. But non-module global scripts cannot access types declared in modules. Enter import types.

在类型注释中使用 import("mod") 允许在模块中访问其导出声明,而无需导入它。

¥Using import("mod") in a type annotation allows for reaching in a module and accessing its exported declaration without importing it.

示例

¥Example

给定模块文件中类 Pet 的声明:

¥Given a declaration of a class Pet in a module file:

ts
// module.d.ts
export declare class Pet {
name: string;
}

可在非模块文件 global-script.ts 中使用:

¥Can be used in a non-module file global-script.ts:

ts
// global-script.ts
function adopt(p: import("./module").Pet) {
console.log(`Adopting ${p.name}...`);
}

.js 中,JSDoc 注释中引用其他模块的类型时,也可以这样做:

¥This also works in JSDoc comments to refer to types from other modules in .js:

js
// a.js
/**
* @param p { import("./module").Pet }
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}

放宽声明触发可见性规则

¥Relaxing declaration emit visibility rules

有了 import 类型,编译器可以处理声明文件生成过程中报告的许多可见性错误,而无需更改输入。

¥With import types available, many of the visibility errors reported during declaration file generation can be handled by the compiler without the need to change the input.

例如:

¥For instance:

ts
import { createHash } from "crypto";
export const hash = createHash("sha256");
// ^^^^
// Exported variable 'hash' has or is using name 'Hash' from external module "crypto" but cannot be named.

在 TypeScript 2.9 中,不会报告任何错误,现在生成的文件如下所示:

¥With TypeScript 2.9, no errors are reported, and now the generated file looks like:

ts
export declare const hash: import("crypto").Hash;

支持 import.meta

¥Support for import.meta

TypeScript 2.9 引入了对 import.meta 的支持,import.meta 是当前 TC39 提案 中描述的新元属性。

¥TypeScript 2.9 introduces support for import.meta, a new meta-property as described by the current TC39 proposal.

import.meta 的类型是全局的 ImportMeta 类型,该类型在 lib.es5.d.ts 中定义。这个接口非常有限。为 Node 或浏览器添加常用属性需要合并接口,并且可能需要根据上下文进行全局扩展。

¥The type of import.meta is the global ImportMeta type which is defined in lib.es5.d.ts. This interface is extremely limited. Adding well-known properties for Node or browsers requires interface merging and possibly a global augmentation depending on the context.

示例

¥Example

假设 __dirnameimport.meta 上始终可用,则声明将通过重新打开 ImportMeta 接口来完成:

¥Assuming that __dirname is always available on import.meta, the declaration would be done through reopening ImportMeta interface:

ts
// node.d.ts
interface ImportMeta {
__dirname: string;
}

用法如下:

¥And usage would be:

ts
import.meta.__dirname; // Has type 'string'

import.meta 仅在针对 ESNext 模块和 ECMAScript 目标时可用。

¥import.meta is only allowed when targeting ESNext modules and ECMAScript targets.

新的 --resolveJsonModule

¥New --resolveJsonModule

在 Node.js 应用中,通常需要 .json。在 TypeScript 2.9 中,resolveJsonModule 允许导入、提取类型以及生成 .json 文件。

¥Often in Node.js applications a .json is needed. With TypeScript 2.9, resolveJsonModule allows for importing, extracting types from and generating .json files.

示例

¥Example

ts
// settings.json
{
"repo": "TypeScript",
"dry": false,
"debug": false
}
ts
// a.ts
import settings from "./settings.json";
settings.debug === true; // OK
settings.dry === 2; // Error: Operator '===' cannot be applied boolean and number
// tsconfig.json
{
"": "commonjs",
}
}

默认输出 --pretty

¥--pretty output by default

从 TypeScript 2.9 开始,如果输出设备支持彩色文本,则默认错误显示在 pretty 下。TypeScript 将检查输出流是否设置了 isTty 属性。

¥Starting TypeScript 2.9 errors are displayed under pretty by default if the output device is applicable for colorful text. TypeScript will check if the output stream has isTty property set.

在命令行中使用 --pretty false 或在 tsconfig.json 中设置 "pretty": false 以禁用 pretty 输出。

¥Use --pretty false on the command line or set "pretty": false in your tsconfig.json to disable pretty output.

新的 --declarationMap

¥New --declarationMap

同时启用 declarationMapdeclaration 会导致编译器在输出 .d.ts 文件的同时生成 .d.ts.map 文件。语言服务现在也可以理解这些映射文件,并使用它们将基于声明文件的定义位置映射到其原始源(如果可用)。

¥Enabling declarationMap alongside declaration causes the compiler to emit .d.ts.map files alongside the output .d.ts files. Language Services can also now understand these map files, and uses them to map declaration-file based definition locations to their original source, when available.

换句话说,在使用 declarationMap 生成的 .d.ts 文件中的声明上点击“跳转定义”按钮,会跳转到定义该声明的源文件 (.ts) 位置,而不是 .d.ts

¥In other words, hitting go-to-definition on a declaration from a .d.ts file generated with declarationMap will take you to the source file (.ts) location where that declaration was defined, and not to the .d.ts.