Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

TypeScript 2.9

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

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

更改包括:

  • 对于某些类型 T,索引类型 keyof Tstring | number | symbol 的子类型。
  • 映射类型 { [P in K]: XXX } 允许任何可赋值给 string | number | symbolK
  • 在针对泛型类型 T 的对象的 for...in 语句中,迭代变量的推断类型以前是 keyof T,但现在变成了 Extract<keyof T, string>。(换句话说,是 keyof T 中仅包含类字符串值的子集。)

给定一个对象类型 Xkeyof X 按如下方式解析:

  • 如果 X 包含字符串索引签名,则 keyof Xstringnumber 以及表示类似 symbol 属性的字面量类型的联合,否则
  • 如果 X 包含数字索引签名,则 keyof Xnumber 以及表示类似字符串和类似 symbol 属性的字面量类型的联合,否则
  • keyof X 是表示类似字符串、类似数字和类似 symbol 属性的字面量类型的联合。

其中:

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

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

示例
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; // 类似字符串名称
  5: string; // 类似数字名称
  [c]: string; // 类似字符串名称
  [d]: string; // 类似数字名称
  [e]: string; // 类似 symbol 名称
  [E1.A]: string; // 类似数字名称
  [E2.A]: string; // 类似字符串名称
};

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> 之类的映射类型在应用于具有数字索引签名的对象类型时能正确工作:

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]; // 以前类型为 any(或在 --noImplicitAny 下报错)

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

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(即 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); // 返回 "Name C"
let x2 = getValue(symbolToNumberMap, sym3); // 返回 3

这是一个破坏性更改;以前,keyof 运算符和映射类型仅支持 string 命名的属性。 使用 keyof T 类型值总是 string 的代码,现在将标记为错误。

示例
ts
function useKey<T, K extends keyof T>(o: T, k: K) {
  var name: string = k; // 错误:keyof T 不能赋值给 string
}

建议

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

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

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

JSX 元素中的泛型类型参数

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

示例
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} />; // 错误

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

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

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

示例
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;

// 推断失败,因为 'number' 和 'string' 都是冲突的候选
let a = tag<string | number>`${100} ${"hello"}`;

import 类型

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

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

示例

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

ts
// module.d.ts

export declare class Pet {
  name: string;
}

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

ts
// global-script.ts

function 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 类型,在声明文件生成期间报告的许多可见性错误现在可以由编译器处理,而无需更改输入。

例如:

ts
import { createHash } from "crypto";

export const hash = createHash("sha256");
//           ^^^^
// 导出的变量 'hash' 具有或使用了外部模块 "crypto" 中的名称 'Hash',但无法命名。

使用 TypeScript 2.9,不会报告错误,并且现在生成的文件如下所示:

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

支持 import.meta

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

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

示例

假设 __dirname 总是在 import.meta 上可用,可以通过重新打开 ImportMeta 接口来完成声明:

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

然后使用:

ts
import.meta.__dirname; // 类型为 '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.ts

import settings from "./settings.json";

settings.debug === true; // OK
settings.dry === 2; // 错误:操作符 '===' 不能应用于 boolean 和 number
json
// 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 文件。 语言服务现在也可以理解这些映射文件,并在可用时使用它们将基于声明文件的定义位置映射到其原始源位置。

换句话说,在由 declarationMap 生成的 .d.ts 文件中的声明上点击“转到定义”将带你到定义该声明的源文件(.ts)位置,而不是 .d.ts