keyof
功能增强
keyof
和映射类型支持 number
和 symbol
类型的属性名称。
在索引类型和映射类型中,TypeScript 2.9 新增了对 number
和 symbol
类型属性名称的支持。在之前的版本,keyof
和映射类型只支持 string
类型的属性名称。
新功能带来的变化包括:
- 对于类型
T
,keyof T
的索引类型是string | number | symbol
的子类型 - 映射类型
{ [P in K]: XXX }
会映射所有兼容string | number | symbol
的K
- 对于
for...in
语句中泛型为T
的对象,迭代变量的类型在之前推断为keyof T
,但是现在推断为Extract<keyof T, string>
(即,只包含keyof T
中的类字符串值)
给定一个对象类型 X
,keyof X
的类型求值过程如下:
- 如果
X
包含字符串索引签名,那么keyof X
的值为string
、number
和类符号属性名的字面量类型组成的联合类型,否则下一步 - 如果
X
包含数值索引签名,那么keyof X
的值为number
和类字符串与类符号属性名的字面量类型组成的联合类型,否则下一步 -
keyof X
的值为类字符串、类数值和类符号属性名的字面量类型组成的联合类型
其中,
- 对象的类字符串(string-like)属性名包括标识符、字符串字面量和字符串字面量类型的计算属性名
- 对象的类数值(number-like)属性名包括数值字面量和数值字面量类型的计算属性名
- 对象的类符号(symbol-like)属性名包括符号类型的计算属性名
在映射类型 { [P in K]: XXX }
中,K
中的每一个字符串字面量类型都会引入一个字符串名称的类型,每一个数值类型字面量类型都会引入一个数值名称的类型,每一个符号类型都会引入一个符号名称的类型。此外,如果 K
包含 string
类型,那么会引入一个字符串索引签名,如果包含 number
类型,也会引入一个数值索引签名。
上面的文字可能不太好理解,来看看下面的例子:
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,标识符
5: string; // number-like, 数值字面量
[c]: string; // string-like, 字符串字面量类型的计算属性
[d]: string; // number-like, 数值字面量类型的计算属性
[e]: string; // symbol-like, 符号类型的计算属性
[E1.A]: string; // number-like, 数值枚举类型的计算属性
[E2.A]: string; // string-like, 字符串枚举类型的计算属性
}
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>
的映射类型能够正确地处理对象类型中的数值索引签名了。
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]; // x 推断为 string,之前版的版本会推断为 any
此外,得益于 keyof
操作支持使用 number
和 symbol
命名的键值,当一个对象使用数值字面量(如数值枚举类型)和唯一符号作为属性索引时,现在我们可以把访问对象属性的过程独立地抽象出来了。
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); // x1 类型为 string ,返回 "Name C"
let x2 = getValue(symbolToNumberMap, sym3); // x1 类型为 number ,返回 3
这是一项重大功能变更。在之前的版本中,keyof
和映射类型只支持 string
类型命名的属性。现在,如果还认为 keyof T
的类型值总是 string
的话,那么将会抛出错误,因为此时 keyof T
的类型值为 string | number | symbol
。举例如下:
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string = k; // Error: keyof T is not assignable to string
}
针对该重大功能变更,有以下建议:
- 如果函数只允许处理
string
类属性名,那么可以使用Extract<keyof T, string>
:
function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
var name: string = k; // OK
}
- 如果函可以处理其他类型的属性键名,那么可以使用如下的做法:
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string | number | symbol = k;
}
- 否则,使用
--keyofStringsOnly
编译选项来禁用新功能
JSX 泛型参数
现在,JSX 支持传递泛型参数给泛型组件了。
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
标签模板泛型参数
标签模板是 ES2015 引入的一种新调用方式。跟调用表达式一样,泛型函数也可用在标签模板中,TypeScript 会根据类型参数进行类型推断。
TypeScript 2.9 支持向标签模板字符串传递泛型类型参数。
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;
// a 的类型为 string | number
let a = tag<string | number> `${100} ${"hello"}`;
import
类型
模块可以导入其他模块的类型声明。但是,非模块化的全局脚本无法访问模块的类型声明。import
为此打开了新世界的大门。
通过在类型注释中使用 import("mod")
,我们可以访问到该模块导出的类型声明,同时模块代码也并不会因此而引入。
来看一个简单的例子。
假如模块中有以下声明:
// module.d.ts
export declare class Pet {
name: string;
}
那么就可以在非模块化的全局脚本中按以下方式使用它:
// global-script.ts
function adopt(p: import("./module").Pet) {
console.log(`Adopting ${p.name}...`);
}
也可以在 JSDoc 注释中使用它:
// a.js
/**
* @param p { import("./module").Pet }
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}
消除声明错误
支持 import
类型后,声明文件生成过程中抛出的许多错误可以由编译器直接处理,而不需要改变原输入文件。
例如:
import { createHash } from "crypto";
export const hash = createHash("sha256");
// ^^^^
// 导出变量 'hash' 已经或正在使用外部模块 "crypto" 中的名称 'Hash',但无法对其命名
在 TypeScript 2.9 中,这样的错误就不会抛出来了,并且生成的声明文件如下:
export declare const hash: import("crypto").Hash;
支持 import.meta
TypeScript 2.9 引入了对 import.meta
的支持,它是 TC39 提案中的一种新的元属性(meta-property)。
import.meta
的类型由全局类型 ImportMeta
所定义,位于 lib.es5.d.ts
。该接口的使用范围是很受限的,主要用来为 Node 或者浏览器添加众所周知的属性,以及可能根据上下文进行的全局增强。
例如,假设 __dirname
在 import.meta
中总是可用的,那么就可以通过 ImportMeta
接口来新增该属性:
// node.d.ts
interface ImportMeta {
__dirname: string;
}
使用方法如下:
import.meta.__dirname // 类型为 'string'
import.meta
只能在编译输出为 ESNext
模块和 ECMAScript 时使用。
编译选项
--resolveJsonModule
在 Node.js 应用中,通常都会使用 .json
文件。在 TypeScript 2.9 中,--resolveJsonModule
编译选项可允许从 .json
中导入、导出其类型。
// settings.json
{
"repo": "TypeScript",
"dry": false,
"debug": false
}
import settings from "./settings.json";
settings.debug === true; // OK
settings.dry === 2; // Error: '===' 不能用于比较 boolean 和 number 类型
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"resolveJsonModule": true,
"esModuleInterop": true
}
}
--pretty
自 TypeScript 2.9 开始,如果输出设备支持多颜色文本,错误信息将默认开启 --pretty
选项。TypeScript 会自动检查输出流是否设置 isTty
属性。
可以在命令行使用 --pretty false
或者 tsconfig.json
中设置 "pretty": false
来禁用 --pretty
输出。
--declarationMap
如果在开启 --declaration
的情况下,同时开启 --declarationMap
,那么编译器会同时生成 .d.ts
和 .d.ts.map
文件。语言服务现在能够正确识别这些映射文件,并且使用它们来映射到源码。
也就是说,在使用“跳到定义之处”功能时,会直接跳转到源码文件,而不是 .d.ts
文件。