TypeScript 2.2

支持混合 (Mix-in) 类

TypeScript 2.2 增加了对 ECMAScript 2015 混合类模式 (见 MDN 混合类的描述JavaScript 类的 “真” 混合 了解更多) 以及使用交叉来类型表达结合混合构造函数的签名及常规构造函数签名的规则.

首先是一些术语:

  • 混合构造函数类型指仅有单个构造函数签名, 且该签名仅有一个类型为 any[] 的变长参数, 返回值为对象类型. 比如, 有 X 为对象类型, new (...args: any[]) => X 是一个实例类型为 X 的混合构造函数类型.

  • 混合类指一个 extends (扩展) 了类型参数类型的表达式的类声明或表达式[1]. 以下规则对混合类声明适用:

    • extends 表达式的类型参数类型必须是混合构造函数.
    • 混合类的构造函数 (如果有) 必须有且仅有一个类型为 any[] 的变长参数, 并且必须使用展开运算符在 super(...args) 调用中将这些参数传递.

假设有类型参数为 T 且约束为 X 的表达式 Base, 处理混合类 class C extends Base {...} 时会假设 BaseX 类型, 处理结果为交叉类型 typeof C & T. 换言之, 一个混合类被表达为混合类构造函数类型与参数基类构造函数类型的交叉类型.

在获取一个包含了混合构造函数类型的交叉类型的构造函数签名时, 混合构造函数签名会被丢弃, 而它们的实例类型会被混合到交叉类型中其他构造函数签名的返回类型中. 比如, 交叉类型 { new(...args: any[]) => A } & { new(s: string) => B } 仅有一个构造函数签名 new(s: string) => A & B.

将以上规则放到一个例子中:

  1. class Point {
  2. constructor(public x: number, public y: number) {}
  3. }
  4. class Person {
  5. constructor(public name: string) {}
  6. }
  7. type Constructor<T> = new(...args: any[]) => T;
  8. function Tagged<T extends Constructor<{}>>(Base: T) {
  9. return class extends Base {
  10. _tag: string;
  11. constructor(...args: any[]) {
  12. super(...args);
  13. this._tag = "";
  14. }
  15. }
  16. }
  17. const TaggedPoint = Tagged(Point);
  18. let point = new TaggedPoint(10, 20);
  19. point._tag = "你好";
  20. class Customer extends Tagged(Person) {
  21. accountBalance: number;
  22. }
  23. let customer = new Customer("张三");
  24. customer._tag = "测试";
  25. customer.accountBalance = 0;

混合类可以通过在类型参数中限定构造函数签名的返回值类型来限制它们可以被混入的类的类型. 举例来说, 下面的 WithLocation 函数实现了一个为满足 Point 接口 (也就是有类型为 numberxy 属性) 的类添加 getLocation 方法的子类工厂.

  1. interface Point {
  2. x: number;
  3. y: number;
  4. }
  5. const WithLocation = <T extends Constructor<Point>>(Base: T) =>
  6. class extends Base {
  7. getLocation(): [number, number] {
  8. return [this.x, this.y];
  9. }
  10. }

object 类型

TypeScript 之前没有一个用来表示非原始类型, 也就是非 number | string | boolean | symbol | null | undefined 的类型. 进入新的 object 类型.

有了 object 类型, Object.create 这样的 API 可以被更好地表达. 比如:

  1. declare function create(o: object | null): void;
  2. create({ prop: 0 }); // 正确
  3. create(null); // 正确
  4. create(42); // 错误
  5. create("string"); // 错误
  6. create(false); // 错误
  7. create(undefined); // 错误

new.target 的支持

new.target 元属性是 ES2015 中引入的新语法. 当一个构造函数的实例通过 new 被创建时, new.target 被设置为到最初被用来配置这个实例的构造函数的引用. 如果一个函数是被调用而不是通过 new 来构造, new.target 则被设置为 undefined.

new.target 在类的构造函数中需要 Object.setPrototypeOf 或者设置 __proto__ 时很有用. 一个具体的例子则是从 Node.js v4 及更高版本中继承 Error.

例子

  1. class CustomError extends Error {
  2. constructor(message?: string) {
  3. super(message); // 'Error' 会改变原型链
  4. Object.setPrototypeOf(this, new.target.prototype); // 恢复原型链
  5. }
  6. }

生成的 JS

  1. var CustomError = (function (_super) {
  2. __extends(CustomError, _super);
  3. function CustomError() {
  4. var _newTarget = this.constructor;
  5. var _this = _super.apply(this, arguments); // 'Error' 会改变原型链
  6. _this.__proto__ = _newTarget.prototype; // 恢复原型链
  7. return _this;
  8. }
  9. return CustomError;
  10. })(Error);

在编写可构建的函数时, 使用 new.target 也很方便, 比如:

  1. function f() {
  2. if (new.target) { /* 通过 'new' 来调用 */ }
  3. }

会被编译为:

  1. function f() {
  2. var _newTarget = this && this instanceof f ? this.constructor : void 0;
  3. if (_newTarget) { /* 通过 'new' 来调用 */ }
  4. }

更好地检查表达式操作数中的 null/undefined

TypeScript 2.2 改进了对表达式中可空的操作数的检查. 具体来说, 下面的例子现在会被标记为错误:

  • 如果 + 运算符的任意操作数为可空的, 且没有任何一个操作数的类型为 anystring.
  • 如果 -, *, **, /, %, <<, >>, >>>, &, |, 或 ^ 运算符的任意操作数为可空的.
  • 如果 <, >, <=, >=, 或 in 运算符的任意操作数为可空的.
  • 如果 instanceof 运算符的右操作数为可空的.
  • 如果 +, -, ~, ++, 或 -- 一元运算符的操作数为可空的.

如果一个操作数的类型是 nullundefined 或包含 nullundefined 的联合类型, 那么这个操作数被认为是可空的. 注意联合类型的情况只会在 --strictNullChecks 时存在, 因为在普通类型检查模式下 nullundefined 会从联合类型中消失掉.

有字符串索引签名类型的点属性

有字符串索引签名的类型可以使用 [] 符号来索引, 但之前不能使用 . 来访问. 从 TypeScript 2.2 开始, 两种方式都被允许.

  1. interface StringMap<T> {
  2. [x: string]: T;
  3. }
  4. const map: StringMap<number>;
  5. map["prop1"] = 1;
  6. map.prop2 = 2;

这一项只会被应用到具备显式字符串索引签名的类型. 使用 . 符号来访问一个类型的未知属性依然会报错.

支持在对 JSX 子元素使用展开运算符

TypeScript 2.2 添加了对 JSX 子元素使用展开运算符的支持. 请参考 facebook/jsx# 57 了解详情.

例子

  1. function Todo(prop: { key: number, todo: string }) {
  2. return <div>{prop.key.toString() + prop.todo}</div>;
  3. }
  4. function TodoList({ todos }: TodoListProps) {
  5. return <div>
  6. {...todos.map(todo => <Todo key={todo.id} todo={todo.todo} />)}
  7. </div>;
  8. }
  9. let x: TodoListProps;
  10. <TodoList {...x} />

新的 jsx: react-native

React-native 的构建过程需要所有文件的扩展名都为 .js, 即使这个文件包含了 JSX 语法. 新的 --jsx 选项值 react-native 将会在生成的文件中保留 JSX 语法, 但使用 .js 扩展名.