TypeScript 2.9

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

TypeScript 2.9增加了在索引类型和映射类型上支持用numbersymbol命名属性。 在之前,keyof操作符和映射类型只支持string命名的属性。

改动包括:

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

对于对象类型Xkeyof X将按以下方式解析:

  • 如果X带有字符串索引签名,则keyof Xstringnumber和表示symbol-like属性的字面量类型的联合,否则
  • 如果X带有数字索引签名,则keyof Xnumber和表示string-like和symbol-like属性的字面量类型的联合,否则
  • keyof X为表示string-like,number-like和symbol-like属性的字面量类型的联合。

在何处:

  • 对象类型的string-like属性,是那些使用标识符,字符串字面量或计算后值为字符串字面量类型的属性名所声明的。
  • 对象类型的number-like属性是那些使用数字字面量或计算后值为数字字面量类型的属性名所声明的。
  • 对象类型的symbol-like属性是那些使用计算后值为symbol字面量类型的属性名所声明的。

对于映射类型{ [P in K]: XXX }K的每个字符串字面量类型都会引入一个名字为字符串的属性,K的每个数字字面量类型都会引入一个名字为数字的属性,K的每个symbol字面量类型都会引入一个名字为symbol的属性。 并且,如果K包含string类型,那个同时也会引入字符串索引类型,如果K包含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 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

现在通过在键值类型里包含number类型,keyof就能反映出数字索引签名的存在,因此像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];  // Previously of type any (or an error with --noImplicitAny)

此外,由于keyof支持用numbersymbol命名的键值,现在可以对对象的数字字面量(如数字枚举类型)和唯一的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);  // Returns "Name C"
let x2 = getValue(symbolToNumberMap, sym3);  // Returns 3

这是一个破坏性改动;之前,keyof操作符和映射类型只支持string命名的属性。 那些把总是把keyof T的类型当做string的代码现在会报错。

例子

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

推荐

  • 如果函数只能处理字符串命名属性的键,在声明里使用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

泛型标记模版里的泛型参数

标记模版是ECMAScript 2015引入的一种调用形式。 类似调用表达式,可以在标记模版里使用泛型函数,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;

// inference fails because 'number' and 'string' are both candidates that conflict
let a = tag<string | number> `${100} ${"hello"}`;

import类型

模块可以导入在其它模块里声明的类型。但是非模块的全局脚本不能访问模块里声明的类型。这里,import类型登场了。

在类型注释的位置使用import("mod"),就可以访问一个模块和它导出的声明,而不必导入它。

例子

在一个模块文件里,有一个Pet类的声明:

// module.d.ts

export declare class Pet {
   name: string;
}

它可以被用在非模块文件global-script.ts

// global-script.ts

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

它也可以被放在.js文件的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");
//           ^^^^
// Exported variable 'hash' has or is using name 'Hash' from external module "crypto" but cannot be named.

TypeScript 2.9不会报错,生成文件如下:

export 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接口来进行声明:

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

用法如下:

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

import.meta仅在输出目标为ESNext模块和ECMAScript时才生效。

新的--resolveJsonModule

在Node.js应用里经常需要使用.json。TypeScript 2.9的--resolveJsonModule允许从.json文件里导入,获取类型。

例子

// settings.json

{
    "repo": "TypeScript",
    "dry": false,
    "debug": false
}
// a.ts

import settings from "./settings.json";

settings.debug === true;  // OK
settings.dry === 2;  // Error: Operator '===' cannot be applied boolean and 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。 语言服务现在也能够理解这些map文件,将声明文件映射到源码。

换句话说,在启用了--declarationMap后生成的.d.ts文件里点击go-to-definition,将会导航到源文件里的位置(.ts),而不是导航到.d.ts文件里。