在 keyof 和映射类型中支持 number 和 symbol 命名的属性
TypeScript 2.9 增加了对索引类型和映射类型中 number 和 symbol 命名属性的支持。此前,keyof 操作符和映射类型仅支持 string 命名的属性。
变更包括
- 对于某些类型
T,索引类型keyof T是string | number | symbol的子类型。 - 映射类型
{ [P in K]: XXX }允许任何可赋值给string | number | symbol的K。 - 在泛型
T对象的for...in语句中,迭代变量的推断类型此前为keyof T,现在变为Extract<keyof T, string>。(换句话说,keyof T中仅包含类字符串值的子集。)
给定一个对象类型 X,keyof X 的解析规则如下
- 如果
X包含字符串索引签名,则keyof X是string、number以及表示 symbol 类属性的字面量类型的联合,否则 - 如果
X包含数字索引签名,则keyof X是number以及表示字符串类和 symbol 类属性的字面量类型的联合,否则 keyof X是表示字符串类、数字类和 symbol 类属性的字面量类型的联合。
其中
- 对象类型的字符串类属性是指那些使用标识符、字符串字面量或字符串字面量类型的计算属性名声明的属性。
- 对象类型的数字类属性是指那些使用数字字面量或数字字面量类型的计算属性名声明的属性。
- 对象类型的 symbol 类属性是指那些使用唯一 symbol 类型(unique symbol type)的计算属性名声明的属性。
在映射类型 { [P in K]: XXX } 中,K 中的每个字符串字面量类型引入一个具有字符串名称的属性,每个数字字面量类型引入一个具有数字名称的属性,每个唯一 symbol 类型引入一个具有唯一 symbol 名称的属性。此外,如果 K 包含 string 类型,则会引入字符串索引签名;如果 K 包含 number 类型,则会引入数字索引签名。
示例
tsconst 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 name5: 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.Atype K2 = Extract<keyof Foo, string>; // "a" | "c" | E2.Atype K3 = Extract<keyof Foo, number>; // 5 | 10 | E1.Atype K4 = Extract<keyof Foo, symbol>; // typeof e
由于 keyof 现在通过在键类型中包含 number 类型来反映数字索引签名的存在,因此像 Partial<T> 和 Readonly<T> 这样的映射类型在应用于带有数字索引签名的对象类型时可以正常工作。
tstype 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 操作符对 number 和 symbol 命名键的支持,现在可以抽象访问那些由数字字面量(例如数字枚举类型)和唯一 symbol 索引的对象属性。
tsconst 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 sym3function 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 的代码,现在将被标记为错误。
示例
tsfunction useKey<T, K extends keyof T>(o: T, k: K) {var name: string = k; // Error: keyof T is not assignable to string}
建议
-
如果您的函数只能处理字符串命名的属性键,请在声明中使用
Extract<keyof T, string>tsfunction useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {var name: string = k; // OK} -
如果您的函数可以处理所有属性键,那么应该在下游进行相应的更改
tsfunction useKey<T, K extends keyof T>(o: T, k: K) {var name: string | number | symbol = k;} -
否则,请使用
keyofStringsOnly编译器选项来禁用此新行为。
JSX 元素中的泛型类型参数
JSX 元素现在允许将类型参数传递给泛型组件。
示例
tsclass GenericComponent<P> extends React.Component<P> {internalProp: P;}type Props = { a: number; b: string };const x = <GenericComponent<Props> a={10} b="hi" />; // OKconst y = <GenericComponent<Props> a={10} b={20} />; // Error
泛型标记模板中的泛型类型参数
标记模板(Tagged templates)是 ECMAScript 2015 引入的一种调用形式。与调用表达式一样,泛型函数可以在标记模板中使用,TypeScript 将推断所使用的类型参数。
TypeScript 2.9 允许将泛型类型参数传递给标记模板字符串。
示例
tsdeclare 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 conflictlet a = tag<string | number>`${100} ${"hello"}`;
import 类型
模块可以导入其他模块中声明的类型。但非模块的全局脚本无法访问模块中声明的类型。于是,import 类型登场了。
在类型注解中使用 import("mod"),无需显式导入即可获取模块并访问其导出的声明。
示例
假设在模块文件中有一个类 Pet 的声明
ts// module.d.tsexport declare class Pet {name: string;}
可以在非模块文件 global-script.ts 中使用
ts// global-script.tsfunction adopt(p: import("./module").Pet) {console.log(`Adopting ${p.name}...`);}
这也适用于 JSDoc 注释,以在 .js 文件中引用其他模块的类型
js// a.js/*** @param p { import("./module").Pet }*/function walk(p) {console.log(`Walking ${p.name}...`);}
放宽声明发射可见性规则
有了 import 类型,声明文件生成过程中报告的许多可见性错误都可以由编译器处理,而无需更改输入。
例如:
tsimport { 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,不再报告错误,生成的文件现在如下所示
tsexport declare const hash: import("crypto").Hash;
支持 import.meta
TypeScript 2.9 引入了对 import.meta 的支持,这是一种由当前 TC39 提案 所描述的新元属性。
import.meta 的类型是全局 ImportMeta 类型,定义在 lib.es5.d.ts 中。此接口极其有限。根据上下文,添加 Node 或浏览器的通用属性需要接口合并,并可能需要全局扩充。
示例
假设 __dirname 在 import.meta 上总是可用,声明可以通过重新打开 ImportMeta 接口来完成
ts// node.d.tsinterface ImportMeta {__dirname: string;}
使用方式如下
tsimport.meta.__dirname; // Has type 'string'
import.meta 仅在目标为 ESNext 模块和 ECMAScript 版本时才允许使用。
新的 --resolveJsonModule
Node.js 应用通常需要 .json 文件。通过 TypeScript 2.9,resolveJsonModule 允许导入、提取 .json 文件的类型并生成它们。
示例
ts// settings.json{"repo": "TypeScript","dry": false,"debug": false}
ts// a.tsimport settings from "./settings.json";settings.debug === true; // OKsettings.dry === 2; // Error: Operator '===' cannot be applied boolean and number
// tsconfig.json{"": {"": "commonjs","": true,"": true}}
默认开启 --pretty 输出
从 TypeScript 2.9 开始,如果输出设备支持彩色文本,错误信息将默认在 pretty 模式下显示。TypeScript 会检查输出流是否设置了 isTty 属性。
在命令行中使用 --pretty false 或在 tsconfig.json 中设置 "pretty": false 来禁用 pretty 输出。
新的 --declarationMap
同时启用 declarationMap 和 declaration 会使编译器在输出 .d.ts 文件时同步生成 .d.ts.map 文件。语言服务现在也可以理解这些映射文件,并在可用时使用它们将基于声明文件的定义位置映射到其原始源文件。
换句话说,对使用 declarationMap 生成的 .d.ts 文件中的声明进行“转到定义”(go-to-definition)操作时,将带您跳转到该声明定义的源文件(.ts)位置,而不是 .d.ts 文件。