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

TypeScript 2.1

keyof 和查找类型

在 JavaScript 中,具有期望属性名称作为参数的 API 相当常见,但到目前为止还无法表达这些 API 中发生的类型关系。

引入索引类型查询或 keyof; 索引类型查询 keyof T 生成 T 允许的属性名称的类型。 keyof T 类型被认为是 string 的子类型。

示例
ts
interface Person {
  name: string;
  age: number;
  location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string

与此相对的是索引访问类型,也称为查找类型。 在语法上,它们看起来就像元素访问,但作为类型编写:

示例
ts
type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string

你可以将这种模式与类型系统的其他部分一起使用,以获得类型安全的查找。

ts
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]; // 推断类型为 T[K]
}

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
  obj[key] = value;
}

let x = { foo: 10, bar: "hello!" };

let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string

let oops = getProperty(x, "wargarbl"); // 错误!"wargarbl" 不是 "foo" | "bar"

setProperty(x, "foo", "string"); // 错误!期望 number,得到 string

映射类型

一个常见的任务是获取一个现有类型并使其每个属性完全可选。 假设我们有一个 Person

ts
interface Person {
  name: string;
  age: number;
  location: string;
}

它的一个部分版本将是:

ts
interface PartialPerson {
  name?: string;
  age?: number;
  location?: string;
}

使用映射类型,PartialPerson 可以写为对类型 Person 的泛化转换:

ts
type Partial<T> = {
  [P in keyof T]?: T[P];
};

type PartialPerson = Partial<Person>;

映射类型是通过获取字面量类型的联合,并为新对象类型计算一组属性而产生的。 它们就像 Python 中的列表推导式,但不是产生列表中的新元素,而是在类型中产生新属性。

除了 Partial,映射类型可以表达许多有用的类型转换:

ts
// 保持类型相同,但使每个属性变为只读。
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 属性名称相同,但使值变为 Promise 而不是具体类型
type Deferred<T> = {
  [P in keyof T]: Promise<T[P]>;
};

// 为 T 的属性包装代理
type Proxify<T> = {
  [P in keyof T]: { get(): T[P]; set(v: T[P]): void };
};

PartialReadonlyRecordPick

如前所述,PartialReadonly 是非常有用的构造。 你可以使用它们来描述一些常见的 JS 例程,例如:

ts
function assign<T>(obj: T, props: Partial<T>): void;
function freeze<T>(obj: T): Readonly<T>;

因此,它们现在默认包含在标准库中。

我们还包含了另外两个实用类型:RecordPick

ts
// 从 T 中选取一组属性 K
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;

const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
ts
// 对于类型 T 的每个属性 K,将其转换为 U
function mapObject<K extends string, T, U>(
  obj: Record<K, T>,
  f: (x: T) => U
): Record<K, U>;

const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }

对象展开和剩余

TypeScript 2.1 带来了对 ESnext 展开和剩余的支持。

与数组展开类似,展开对象可以方便地获得浅拷贝:

ts
let copy = { ...original };

同样,你可以合并几个不同的对象。 在以下示例中,merged 将具有来自 foobarbaz 的属性。

ts
let merged = { ...foo, ...bar, ...baz };

你还可以覆盖现有属性并添加新属性:

ts
let obj = { x: 1, y: "string" };
var newObj = { ...obj, z: 3, y: 4 }; // { x: number, y: number, z: number }

指定展开操作的顺序决定了结果对象中最终包含哪些属性; 后面展开中的属性“胜出”先前创建的属性。

对象剩余是对象展开的对偶,它们可以提取解构元素时未选取的任何额外属性:

ts
let obj = { x: 1, y: 1, z: 1 };
let { z, ...obj1 } = obj;
obj1; // {x: number, y:number};

降级异步函数

此功能在 TypeScript 2.1 之前就已支持,但仅在针对 ES6/ES2015 时有效。 TypeScript 2.1 将能力带到了 ES3 和 ES5 运行时,这意味着无论你使用什么环境,你都可以自由地利用它。

注意:首先,我们需要确保我们的运行时在全局范围内有一个符合 ECMAScript 的 Promise。 这可能涉及为 Promise 获取 一个 polyfill,或者依赖于你可能正在使用的运行时中已有的一个。 我们还需要通过将你的 lib 选项设置为例如 "dom", "es2015""dom", "es2015.promise", "es5" 来确保 TypeScript 知道 Promise 存在。

示例

tsconfig.json
json
{
  "compilerOptions": {
    "lib": ["dom", "es2015.promise", "es5"]
  }
}

dramaticWelcome.ts
ts
function delay(milliseconds: number) {
  return new Promise<void>(resolve => {
    setTimeout(resolve, milliseconds);
  });
}

async function dramaticWelcome() {
  console.log("Hello");

  for (let i = 0; i < 3; i++) {
    await delay(500);
    console.log(".");
  }

  console.log("World!");
}

dramaticWelcome();

编译并运行输出应该在 ES3/ES5 引擎上产生正确的行为。

对外部辅助库的支持(tslib

TypeScript 注入了一些辅助函数,例如用于继承的 __extends、用于对象字面量和 JSX 元素中展开运算符的 __assign,以及用于异步函数的 __awaiter

以前有两个选项:

  1. 每个需要它们的文件中注入辅助函数,或者
  2. 使用 noEmitHelpers 完全不使用辅助函数。

这两个选项都不尽如人意; 在每个文件中打包辅助函数是试图保持包体积小的客户的痛点。 而不包含辅助函数意味着客户必须维护自己的辅助库。

TypeScript 2.1 允许将这些文件作为单独的模块在项目中包含一次,编译器将根据需要发出对它们的导入。

首先,安装 tslib 实用库:

sh
npm install tslib

其次,使用 importHelpers 编译你的文件:

sh
tsc --module commonjs --importHelpers a.ts

因此,给定以下输入,生成的 .js 文件将包含对 tslib 的导入,并使用其中的 __assign 辅助函数,而不是内联它。

ts
export const o = { a: 1, name: "o" };
export const copy = { ...o };
js
"use strict";
var tslib_1 = require("tslib");
exports.o = { a: 1, name: "o" };
exports.copy = tslib_1.__assign({}, exports.o);

无类型导入

传统上,TypeScript 对于如何导入模块过于严格。 这是为了避免拼写错误并防止用户错误地使用模块。

然而,很多时候,你可能只想导入一个可能没有自己的 .d.ts 文件的现有模块。 以前这是一个错误。 从 TypeScript 2.1 开始,这变得容易多了。

使用 TypeScript 2.1,你可以在不需要类型声明的情况下导入 JavaScript 模块。 如果存在类型声明(例如 declare module "foo" { ... }node_modules/@types/foo),仍然优先。

在没有声明文件的模块上的导入在 noImplicitAny 下仍会被标记为错误。

示例
ts
// 如果 `node_modules/asdf/index.js` 存在,则成功
import { x } from "asdf";

支持 --target ES2016--target ES2017--target ESNext

TypeScript 2.1 支持三个新的目标值 --target ES2016--target ES2017--target ESNext

使用目标 --target ES2016 将指示编译器不转换 ES2016 特定的特性,例如 ** 运算符。

类似地,--target ES2017 将指示编译器不转换 ES2017 特定的特性,如 async/await

--target ESNext 针对最新的ES 提议特性

改进的 any 推断

以前,如果 TypeScript 无法确定变量的类型,它会选择 any 类型。

ts
let x; // 隐式 'any'
let y = []; // 隐式 'any[]'

let z: any; // 显式 'any'

使用 TypeScript 2.1,TypeScript 不再仅仅选择 any,而是会根据你后续赋值的类型来推断类型。

这仅在设置 noImplicitAny 时启用。

示例
ts
let x;

// 你仍然可以将任何内容赋值给 'x'。
x = () => 42;

// 在最后一次赋值之后,TypeScript 2.1 知道 'x' 的类型是 '() => number'。
let y = x();

// 因此,现在它会告诉你不能将数字添加到函数中!
console.log(x + y);
//          ~~~~~
// 错误!运算符 '+' 不能应用于类型 '() => number' 和 'number'。

// TypeScript 仍然允许你将任何内容赋值给 'x'。
x = "Hello world!";

// 但现在它也知道 'x' 是一个 'string'!
x.toLowerCase();

同样的跟踪现在也应用于空数组。

一个没有类型注解且初始值为 [] 的变量被视为隐式 any[] 变量。 然而,随后的每次 x.push(value)x.unshift(value)x[n] = value 操作都会根据添加到其中的元素演化变量的类型。

ts
function f1() {
  let x = [];
  x.push(5);
  x[1] = "hello";
  x.unshift(true);
  return x; // (string | number | boolean)[]
}

function f2() {
  let x = null;
  if (cond()) {
    x = [];
    while (cond()) {
      x.push("hello");
    }
  }
  return x; // string[] | null
}

隐式 any 错误

这样做的一大好处是,在运行 noImplicitAny 时,你会看到更少的隐式 any 错误。 仅当编译器无法在没有类型注解的情况下知道变量的类型时,才会报告隐式 any 错误。

示例
ts
function f3() {
  let x = []; // 错误:变量 'x' 在无法确定其类型的某些位置隐式具有类型 'any[]'。
  x.push(5);
  function g() {
    x; // 错误:变量 'x' 隐式具有类型 'any[]'。
  }
}

对字面量类型的更好推断

字符串、数字和布尔字面量类型(例如 "abc"1true)以前仅在存在显式类型注解时才被推断。 从 TypeScript 2.1 开始,字面量类型始终被推断为 const 变量和 readonly 属性。

没有类型注解的 const 变量或 readonly 属性推断出的类型是字面量初始值设定项的类型。 带有初始值设定项且没有类型注解的 let 变量、var 变量、参数或非 readonly 属性推断出的类型是初始值设定项的拓宽字面量类型。 其中字符串字面量类型的拓宽类型是 string,数字字面量类型是 numbertruefalseboolean,枚举字面量类型是包含它的枚举。

示例
ts
const c1 = 1; // 类型 1
const c2 = c1; // 类型 1
const c3 = "abc"; // 类型 "abc"
const c4 = true; // 类型 true
const c5 = cond ? 1 : "abc"; // 类型 1 | "abc"

let v1 = 1; // 类型 number
let v2 = c2; // 类型 number
let v3 = c3; // 类型 string
let v4 = c4; // 类型 boolean
let v5 = c5; // 类型 number | string

字面量类型的拓宽可以通过显式类型注解来控制。 具体来说,当在没有类型注解的 const 位置上推断出一个字面量类型的表达式时,该 const 变量会得到一个拓宽的字面量类型推断。 但当 const 位置具有显式字面量类型注解时,该 const 变量会得到一个非拓宽的字面量类型。

示例
ts
const c1 = "hello"; // 拓宽类型 "hello"
let v1 = c1; // 类型 string

const c2: "hello" = "hello"; // 类型 "hello"
let v2 = c2; // 类型 "hello"

将 super 调用的返回值用作 'this'

在 ES2015 中,返回对象的构造函数会隐式地将 this 的值替换为任何调用 super() 的调用者。 因此,有必要捕获 super() 的任何可能返回值并将其替换为 this。 此更改使得与自定义元素一起工作成为可能,它利用这一点用用户编写的构造函数初始化浏览器分配的元素。

示例
ts
class Base {
  x: number;
  constructor() {
    // 返回一个不同于 `this` 的新对象
    return {
      x: 1
    };
  }
}

class Derived extends Base {
  constructor() {
    super();
    this.x = 2;
  }
}

生成:

js
var Derived = (function(_super) {
  __extends(Derived, _super);
  function Derived() {
    var _this = _super.call(this) || this;
    _this.x = 2;
    return _this;
  }
  return Derived;
})(Base);

此更改导致了扩展内置类(如 ErrorArrayMap 等)的行为变化。更多详情请参阅扩展内置类的破坏性更改文档

配置继承

通常一个项目有多个输出目标,例如 ES5ES2015、调试和生产或 CommonJSSystem; 这两个目标之间只有少数配置选项发生变化,维护多个 tsconfig.json 文件可能很麻烦。

TypeScript 2.1 支持使用 extends 继承配置,其中:

  • extendstsconfig.json 中的一个新顶级属性(与 compilerOptionsfilesincludeexclude 并列)。
  • extends 的值必须是一个字符串,包含要继承的另一个配置文件的路径。
  • 首先加载基础文件中的配置,然后由继承配置文件中配置覆盖。
  • 配置文件之间的循环是不允许的。
  • 继承配置文件中的 filesincludeexclude 覆盖基础配置文件中的那些。
  • 配置文件中找到的所有相对路径都将相对于它们所在的配置文件进行解析。

示例

configs/base.json

json
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

tsconfig.json

json
{
  "extends": "./configs/base",
  "files": ["main.ts", "supplemental.ts"]
}

tsconfig.nostrictnull.json

json
{
  "extends": "./tsconfig",
  "compilerOptions": {
    "strictNullChecks": false
  }
}

新的 --alwaysStrict

使用 alwaysStrict 调用编译器会导致:

  1. 以严格模式解析所有代码。
  2. 在每个生成的文件顶部写入 "use strict"; 指令。

模块会自动以严格模式解析。 新标志推荐用于非模块代码。