前言

Repo: https://github.com/zhongsp/TypeScript

该工程是对 TypeScript 官方及开源社区书写的编程手册、版本发布说明等综合内容的中文翻译。 感谢 Microsoft 和开源社区的工程师们的工作,为 JavaScript 开发带来了全新的体验!

这个项目是我在 2015 年创建的,没想到已经维护快 7 年了,它已然是我参与过的时间最长的项目。 在 2015 年之前,我都是在使用 JavaScript 语言,主要参与的项目也大都是采用 AngularJS 框架的项目,没有接触过 TypeScript。 那时候,TypeScript 在国内项目里用的好像不多,但是在国外已经有不少项目开始采用这个新技术。 2015 年,我正好参与了一个和国外一起合作的项目,决定使用 TypeScript 1.x。 也正因为这个机会,我开始了 TypeScript 的学习。 学习没多久,我就喜欢上了这个语言,并且确信这个东西一定能火。 因为作为一个多年的 JavaScript 程序员来讲,我很清楚它解决了多少痛点(必须得把 VS Code 一起代上)。

早些时候,TypeScript 的文档也不多。 原因之一,TypeScript 是 JavaScript 的超集,JavaScript 的知识点已经有足够的资料了,TypeScript 一笔代过。 原因之二,早期的 TypeScript 里特性不多,知识点不多。原因之三,它的文档相较于做的好的语言来讲确实较弱,可能没什么专门的团队负责,或者没有专职的 technical writer 去写作。 于是,我决定边学边翻译,一方面为了自己,另一方面为了其它小伙伴。

哪些内容会继续更新?

我会继续翻译 TypeScript 新版本的 Release Notes。

哪些内容可能不会继续更新?

这个项目中的 Handbook 是翻译老版本的 Handbook。 TypeScript 官网大约从 2020 年开始要打造新版的官网,其中包括官网的样式,以及要重写大部分的文档。 目前,我不打算再翻译一遍新版的 Handbook。 我看了下新版的手册,确实优化了不少,但也不代表老版本是无用的或错误的。

现在,TypeScript 官网也开始支持国际化了,已经有部分文档翻译成了中文,我之前还翻译了一篇。 本着开源和为社区服务的精神,推荐学有余力的同学直接给官网提交翻译的 Pull Reuqest,造福开发者。

关于《TypeScript入门与实战》一书

因为长期维护 TypeScript 更新的内容再加上在项目中一直使用 TypeScript, 所以有机会将知识进行梳理总结成书。

我出版了《TypeScript入门与实战》一书。

TypeScript入门与实战

在该书中,尝试着尽可能完整地介绍TypeScript语言的基础知识,并结合了一些本人的使用经验和体会。 它主要面向的是TypeScript语言的初级和中级使用者。 本人还处于TypeScript语言的学习阶段,可能存在理解错误的地方,还请大家指正,一起进步。 但需要强调的是,本书不是对 Handbook 的翻译。

感谢

在过去的七年中,有很多素不相识、极富开源精神的小伙伴们曾参与到本工程的翻译与校对工作中。 对你们表示感谢!同时也欢迎其它任何想参与到该工程中的朋友们,贡献你们的力量!

快速上手

5分钟了解TypeScript

让我们使用TypeScript来创建一个简单的Web应用。

安装TypeScript

有两种主要的方式来获取TypeScript工具:

  • 通过npm(Node.js包管理器)
  • 安装Visual Studio的TypeScript插件

Visual Studio 2017和Visual Studio 2015 Update 3默认包含了TypeScript。 如果你的Visual Studio还没有安装TypeScript,你可以下载它。

针对使用npm的用户:

> npm install -g typescript

构建你的第一个TypeScript文件

在编辑器,将下面的代码输入到greeter.ts文件里:

function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.textContent = greeter(user);

编译代码

我们使用了.ts扩展名,但是这段代码仅仅是JavaScript而已。 你可以直接从现有的JavaScript应用里复制/粘贴这段代码。

在命令行上,运行TypeScript编译器:

tsc greeter.ts

输出结果为一个greeter.js文件,它包含了和输入文件中相同的JavsScript代码。 一切准备就绪,我们可以运行这个使用TypeScript写的JavaScript应用了!

接下来让我们看看TypeScript工具带来的高级功能。 给person函数的参数添加: string类型注解,如下:

function greeter(person: string) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.textContent = greeter(user);

类型注解

TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式。 在这个例子里,我们希望greeter函数接收一个字符串参数。 然后尝试把greeter的调用改成传入一个数组:

function greeter(person: string) {
    return "Hello, " + person;
}

let user = [0, 1, 2];

document.body.textContent = greeter(user);

重新编译,你会看到产生了一个错误。

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

类似地,尝试删除greeter调用的所有参数。 TypeScript会告诉你使用了非期望个数的参数调用了这个函数。 在这两种情况中,TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。

要注意的是尽管有错误,greeter.js文件还是被创建了。 就算你的代码里有错误,你仍然可以使用TypeScript。但在这种情况下,TypeScript会警告你代码可能不会按预期执行。

接口

让我们开发这个示例应用。这里我们使用接口来描述一个拥有firstNamelastName字段的对象。 在TypeScript里,只要两个类型内部的结构兼容那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用implements语句。

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "User" };

document.body.textContent = greeter(user);

最后,让我们使用类来改写这个例子。 TypeScript支持JavaScript的新特性,比如支持基于类的面向对象编程。

让我们创建一个Student类,它带有一个构造函数和一些公共字段。 注意类和接口可以一起工作,程序员可以自行决定抽象的级别。

还要注意的是,在构造函数的参数上使用public等同于创建了同名的成员变量。

class Student {
    fullName: string;
    constructor(public firstName: string, public middleInitial: string, public lastName: string) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = new Student("Jane", "M.", "User");

document.body.textContent = greeter(user);

重新运行tsc greeter.ts,你会看到生成的JavaScript代码和原先的一样。 TypeScript里的类只是JavaScript里常用的基于原型面向对象编程的简写。

运行TypeScript Web应用

greeter.html里输入如下内容:

<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src="greeter.js"></script>
    </body>
</html>

在浏览器里打开greeter.html运行这个应用!

可选地:在Visual Studio里打开greeter.ts或者把代码复制到TypeScript playground。 将鼠标悬停在标识符上查看它们的类型。 注意在某些情况下它们的类型可以被自动地推断出来。 重新输入一下最后一行代码,看一下自动补全列表和参数列表,它们会根据DOM元素类型而变化。 将光标放在greeter函数上,点击F12可以跟踪到它的定义。 还有一点,你可以右键点击标识,使用重构功能来重命名。

这些类型信息以及工具可以很好的和JavaScript一起工作。 更多的TypeScript功能演示,请查看本网站的示例部分。

ASP.NET Core

ASP.NET Core

安装 ASP.NET Core 和 TypeScript

首先,若有需要请安装 ASP.NET Core。此篇指南需要使用Visual Studio 2015或2017。

其次,如果你的Visual Studio不带有最新版本的TypeScript,你可以从这里安装。

新建工程

  1. 选择 File

  2. 选择 New Project (Ctrl + Shift + N)

  3. 选择 Visual C#

  4. 若使用VS2015,选择 ASP.NET Web Application > ASP.NET 5 Empty,并且取消勾选“Host in the cloud”,因为我们要在本地运行。

    使用空白模版

  5. 若使用VS2017,选择 ASP.NET Core Web Application (.NET Core) > ASP.NET Core 1.1 Empty

    使用空白模版VS2017

运行此应用以确保它能正常工作。

设置服务项

VS2015

project.json 文件的 "dependencies" 字段里添加:

"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final"

最终的 dependencies 部分应该类似于下面这样:

"dependencies": {
  "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
  "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
  "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final"
},

用以下内容替换 Startup.cs 文件里的 Configure 函数:

public void Configure(IApplicationBuilder app)
{
    app.UseIISPlatformHandler();
    app.UseDefaultFiles();
    app.UseStaticFiles();
}

VS2017

打开 Dependencies > Manage NuGet Packages > Browse。搜索并安装Microsoft.AspNetCore.StaticFiles 1.1.2:

安装Microsoft.AspNetCore.StaticFiles

如下替换掉Startup.csConfigure的内容:

public void Configure(IApplicationBuilder app)
{
    app.UseDefaultFiles();
    app.UseStaticFiles();
}

你可能需要重启VS,这样UseDefaultFilesUseStaticFiles下面的波浪线才会消失。

添加 TypeScript

下一步我们为 TypeScript 添加一个文件夹。

Create new folder

将文件夹命名为 scripts

scripts folder

添加 TypeScript 代码

scripts上右击并选择New Item。 接着选择TypeScript File(也可能 .NET Core 部分),并将此文件命名为app.ts

New item

添加示例代码

将以下代码写入app.ts文件。

function sayHello() {
  const compiler = (document.getElementById("compiler") as HTMLInputElement).value;
  const framework = (document.getElementById("framework") as HTMLInputElement).value;
  return `Hello from ${compiler} and ${framework}!`;
}

构建设置

配置 TypeScript 编译器

我们先来告诉TypeScript怎样构建。 右击scripts文件夹并选择New Item。 接着选择TypeScript Configuration File,保持文件的默认名字为tsconfig.json

Create tsconfig.json

将默认的tsconfig.json内容改为如下所示:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "noEmitOnError": true,
    "sourceMap": true,
    "target": "es5"
  },
  "files": [
    "./app.ts"
  ],
  "compileOnSave": true
}

看起来和默认的设置差不多,但注意以下不同之处:

  1. 设置"noImplicitAny": true
  2. 显式列出了"files"而不是依据"excludes"
  3. 设置"compileOnSave": true

当你写新代码时,设置"noImplicitAny"选项是个不错的选择 — 这可以确保你不会错写任何新的类型。 设置"compileOnSave"选项可以确保你在运行web程序前自动编译保存变更后的代码。

配置 NPM

现在,我们来配置NPM以使用我们能够下载JavaScript包。 在工程上右击并选择New Item。 接着选择NPM Configuration File,保持文件的默认名字为package.json。 在"devDependencies"部分添加"gulp"和"del":

"devDependencies": {
  "gulp": "3.9.0",
  "del": "2.2.0"
}

保存这个文件后,Visual Studio将开始安装gulp和del。 若没有自动开始,请右击package.json文件选择Restore Packages

设置 gulp

最后,添加一个新JavaScript文件gulpfile.js。 键入以下内容:

/// <binding AfterBuild='default' Clean='clean' />
/*
This file is the main entry point for defining Gulp tasks and using Gulp plugins.
Click here to learn more. http://go.microsoft.com/fwlink/?LinkId=518007
*/

var gulp = require('gulp');
var del = require('del');

var paths = {
  scripts: ['scripts/**/*.js', 'scripts/**/*.ts', 'scripts/**/*.map'],
};

gulp.task('clean', function () {
  return del(['wwwroot/scripts/**/*']);
});

gulp.task('default', function () {
  gulp.src(paths.scripts).pipe(gulp.dest('wwwroot/scripts'))
});

第一行是告诉Visual Studio构建完成后,立即运行'default'任务。 当你应答 Visual Studio 清除构建内容后,它也将运行'clean'任务。

现在,右击gulpfile.js并选择Task Runner Explorer。 若'default'和'clean'任务没有显示输出内容的话,请刷新explorer:

Refresh Task Runner Explorer

编写HTML页

wwwroot中添加一个新建项 index.html。 在index.html中写入以下代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script src="scripts/app.js"></script>
    <title></title>
</head>
<body>
    <div id="message"></div>
    <div>
        Compiler: <input id="compiler" value="TypeScript" onkeyup="document.getElementById('message').innerText = sayHello()" /><br />
        Framework: <input id="framework" value="ASP.NET" onkeyup="document.getElementById('message').innerText = sayHello()" />
    </div>
</body>
</html>

测试

  1. 运行项目。
  2. 在输入框中键入时,您应该看到一个消息:

Picture of running demo

调试

  1. 在 Edge 浏览器中,按 F12 键并选择 Debugger 标签页。
  2. 展开 localhost 列表,选择 scripts/app.ts
  3. return 那一行上打一个断点。
  4. 在输入框中键入一些内容,确认TypeScript代码命中断点,观察它是否能正确地工作。

Demo paused on breakpoint

这就是你需要知道的在ASP.NET中使用TypeScript的基本知识了。 接下来,我们引入Angular,写一个简单的Angular程序示例。

添加 Angular 2

使用 NPM 下载依赖的包

添加Angular 2和SystemJS到package.jsondependencies里。

对于VS2015,新的dependencies列表如下:

"dependencies": {
  "angular2": "2.0.0-beta.11",
  "systemjs": "0.19.24",
  "gulp": "3.9.0",
  "del": "2.2.0"
},

若使用VS2017,因为NPM3反对同行的依赖(peer dependencies),我们需要把Angular 2同行的依赖也直接列为依赖项:

"dependencies": {
  "angular2": "2.0.0-beta.11",
  "reflect-metadata": "0.1.2",
  "rxjs": "5.0.0-beta.2",
  "zone.js": "^0.6.4",
  "systemjs": "0.19.24",
  "gulp": "3.9.0",
  "del": "2.2.0"
},

更新 tsconfig.json

现在安装好了Angular 2及其依赖项,我们需要启用TypeScript中实验性的装饰器支持。 我们还需要添加ES2015的声明,因为Angular使用core-js来支持像Promise的功能。 在未来,装饰器会成为默认设置,那时也就不再需要这些设置了。

添加"experimentalDecorators": true, "emitDecoratorMetadata": true"compilerOptions"部分。 然后,再添加"lib": ["es2015", "es5", "dom"]"compilerOptions",以引入ES2015的声明。 最后,我们需要添加"./model.ts""files"里,我们接下来会创建它。 现在tsconfig.json看起来如下:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "noEmitOnError": true,
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "es5",
    "lib": [
      "es2015", "es5", "dom"
    ]
  },
  "files": [
    "./app.ts",
    "./model.ts",
    "./main.ts",
  ],
  "compileOnSave": true
}

将 Angular 添加到 gulp 构建中

最后,我们需要确保 Angular 文件作为 build 的一部分复制进来。 我们需要添加:

  1. 库文件目录。
  2. 添加一个 lib 任务来输送文件到 wwwroot
  3. default 任务上添加 lib 任务依赖。

更新后的 gulpfile.js 像如下所示:

/// <binding AfterBuild='default' Clean='clean' />
/*
This file is the main entry point for defining Gulp tasks and using Gulp plugins.
Click here to learn more. http://go.microsoft.com/fwlink/?LinkId=518007
*/

var gulp = require('gulp');
var del = require('del');

var paths = {
    scripts: ['scripts/**/*.js', 'scripts/**/*.ts', 'scripts/**/*.map'],
    libs: ['node_modules/angular2/bundles/angular2.js',
           'node_modules/angular2/bundles/angular2-polyfills.js',
           'node_modules/systemjs/dist/system.src.js',
           'node_modules/rxjs/bundles/Rx.js']
};

gulp.task('lib', function () {
    gulp.src(paths.libs).pipe(gulp.dest('wwwroot/scripts/lib'));
});

gulp.task('clean', function () {
    return del(['wwwroot/scripts/**/*']);
});

gulp.task('default', ['lib'], function () {
    gulp.src(paths.scripts).pipe(gulp.dest('wwwroot/scripts'));
});

此外,保存了此gulpfile后,要确保 Task Runner Explorer 能看到 lib 任务。

用 TypeScript 写一个简单的 Angular 应用

首先,将 app.ts 改成:

import {Component} from "angular2/core"
import {MyModel} from "./model"

@Component({
    selector: `my-app`,
    template: `<div>Hello from {{getCompiler()}}</div>`
})
export class MyApp {
    model = new MyModel();
    getCompiler() {
        return this.model.compiler;
    }
}

接着在scripts中添加TypeScript文件model.ts:

export class MyModel {
    compiler = "TypeScript";
}

再在scripts中添加main.ts

import {bootstrap} from "angular2/platform/browser";
import {MyApp} from "./app";
bootstrap(MyApp);

最后,将index.html改成:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script src="scripts/lib/angular2-polyfills.js"></script>
    <script src="scripts/lib/system.src.js"></script>
    <script src="scripts/lib/rx.js"></script>
    <script src="scripts/lib/angular2.js"></script>
    <script>
    System.config({
        packages: {
            'scripts': {
                format: 'cjs',
                defaultExtension: 'js'
            }
        }
    });
    System.import('scripts/main').then(null, console.error.bind(console));
    </script>
    <title></title>
</head>
<body>
    <my-app>Loading...</my-app>
</body>
</html>

这里加载了此应用。 运行 ASP.NET 应用,你应该能看到一个div显示"Loading..."紧接着更新成显示"Hello from TypeScript"。

ASP.NET 4

ASP.NET 4

注意: 此教程已从官方删除

安装 TypeScript

如果你使用的 Visual Studio 版本还不支持 TypeScript, 你可以安装 Visual Studio 2015 或者 Visual Studio 2013。 这个快速上手指南使用的是 Visual Studio 2015。

新建项目

  1. 选择 File

  2. 选择 New Project

  3. 选择 Visual C#

  4. 选择 ASP.NET Web Application

    Create new ASP.NET project

  5. 选择 MVC

    取消复选 "Host in the cloud" 本指南将使用一个本地示例。 Use MVC template

运行此应用以确保它能正常工作。

添加 TypeScript

下一步我们为 TypeScript 添加一个文件夹。

Create new folder

将文件夹命名为 src。

src folder

添加 TypeScript 代码

src 上右击并选择 New Item。 接着选择 TypeScript File 并将此文件命名为 app.ts

New item

添加示例代码

将以下代码写入 app.ts 文件。

function sayHello() {
    const compiler = (document.getElementById("compiler") as HTMLInputElement).value;
    const framework = (document.getElementById("framework") as HTMLInputElement).value;
    return `Hello from ${compiler} and ${framework}!`;
}

构建设置

右击项目并选择 New Item。 接着选择 TypeScript Configuration File 保持文件的默认名字为 tsconfig.json

Create tsconfig.json

将默认的 tsconfig.json 内容改为如下所示:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "noEmitOnError": true,
    "sourceMap": true,
    "target": "es5",
    "outDir": "./Scripts/App"
  },
  "files": [
    "./src/app.ts",
  ],
  "compileOnSave": true
}

看起来和默认的设置差不多,但注意以下不同之处:

  1. 设置 "noImplicitAny": true
  2. 特别是这里 "outDir": "./Scripts/App"
  3. 显式列出了 "files" 而不是依据 "excludes"选项。
  4. 设置 "compileOnSave": true

当你写新代码时,设置 "noImplicitAny" 选项是个好主意 — 这可以确保你不会错写任何新的类型。 设置 "compileOnSave" 选项可以确保你在运行web程序前自动编译保存变更后的代码。 更多信息请参见 the tsconfig.json documentation

在视图中调用脚本

  1. Solution Explorer 中, 打开 Views | Home | Index.cshtml

    Open Index.cshtml

  2. 修改代码如下:

    @{
        ViewBag.Title = "Home Page";
    }
    <script src="~/Scripts/App/app.js"></script>
    <div id="message"></div>
    <div>
        Compiler: <input id="compiler" value="TypeScript" onkeyup="document.getElementById('message').innerText = sayHello()" /><br />
        Framework: <input id="framework" value="ASP.NET" onkeyup="document.getElementById('message').innerText = sayHello()" />
    </div>
    

测试

  1. 运行项目。
  2. 在输入框中键入时,您应该看到一个消息:

Picture of running demo

调试

  1. 在 Edge 浏览器中, 按 F12 键并选择 Debugger 标签页。
  2. 展开 localhost 列表, 选择 src/app.ts
  3. return 那一行上打一个断点。
  4. 在输入框中键入一些内容,确认TypeScript代码命中断点,观察它是否能正确地工作。

Demo paused on breakpoint

这就是你需要知道的在ASP.NET中使用TypeScript的基本知识了。接下来,我们引入Angular,写一个简单的Angular程序示例。

添加 Angular 2

使用 NPM 下载所需的包

  1. 安装 PackageInstaller

  2. 用 PackageInstaller 来安装 Angular 2, systemjs 和 Typings。

    在project上右击, 选择 Quick Install Package

    Use PackageInstaller to install angular2 Use PackageInstaller to install systemjs Use PackageInstaller to install Typings

  3. 用 PackageInstaller 安装 es6-shim 的类型文件。

    Angular 2 包含 es6-shim 以提供 Promise 支持, 但 TypeScript 还需要它的类型文件。 在 PackageInstaller 中, 选择 Typing 替换 npm 选项。接着键入 "es6-shim":

    Use PackageInstaller to install es6-shim typings

更新 tsconfig.json

现在安装好了 Angular 2 及其依赖项, 我们还需要启用 TypeScript 中实验性的装饰器支持并且引入 es6-shim 的类型文件。 将来的版本中,装饰器和 ES6 选项将成为默认选项,我们就可以不做此设置了。 添加"experimentalDecorators": true, "emitDecoratorMetadata": true选项到"compilerOptions",再添加"./typings/index.d.ts""files"。 最后,我们要新建"./src/model.ts"文件,并且得把它加到"files"里。 现在tsconfig.json应该是这样:

{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "sourceMap": true,
    "target": "es5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./Scripts/App"
  },
  "files": [
    "./src/app.ts",
    "./src/model.ts",
    "./src/main.ts",
    "./typings/index.d.ts"
  ]
}

添加 CopyFiles 到 build 中

最后,我们需要确保 Angular 文件作为 build 的一部分复制进来。这样操作,右击项目选择 'Unload' ,再次右击项目选择 'Edit csproj'。 在 TypeScript 配置项 PropertyGroup 之后,添加一个 ItemGroup 和 Target 配置项来复制 Angular 文件。

<ItemGroup>
  <NodeLib Include="$(MSBuildProjectDirectory)\node_modules\angular2\bundles\angular2.js"/>
  <NodeLib Include="$(MSBuildProjectDirectory)\node_modules\angular2\bundles\angular2-polyfills.js"/>
  <NodeLib Include="$(MSBuildProjectDirectory)\node_modules\systemjs\dist\system.src.js"/>
  <NodeLib Include="$(MSBuildProjectDirectory)\node_modules\rxjs\bundles\Rx.js"/>
</ItemGroup>
<Target Name="CopyFiles" BeforeTargets="Build">
  <Copy SourceFiles="@(NodeLib)" DestinationFolder="$(MSBuildProjectDirectory)\Scripts"/>
</Target>

现在,在工程上右击选择重新加载项目。 此时应当能在解决方案资源管理器(Solution Explorer)中看到node_modules

用 TypeScript 写一个简单的 Angular 应用

首先,将 app.ts 改成:

import {Component} from "angular2/core"
import {MyModel} from "./model"

@Component({
    selector: `my-app`,
    template: `<div>Hello from {{getCompiler()}}</div>`
})
class MyApp {
    model = new MyModel();
    getCompiler() {
        return this.model.compiler;
    }
}

接着在 src 中添加 TypeScript 文件 model.ts:

export class MyModel {
    compiler = "TypeScript";
}

再在 src 中添加 main.ts

import {bootstrap} from "angular2/platform/browser";
import {MyApp} from "./app";
bootstrap(MyApp);

最后,将 Views/Home/Index.cshtml 改成:

@{
    ViewBag.Title = "Home Page";
}
<script src="~/Scripts/angular2-polyfills.js"></script>
<script src="~/Scripts/system.src.js"></script>
<script src="~/Scripts/rx.js"></script>
<script src="~/Scripts/angular2.js"></script>
<script>
    System.config({
        packages: {
            '/Scripts/App': {
                format: 'cjs',
                defaultExtension: 'js'
            }
        }
    });
    System.import('/Scripts/App/main').then(null, console.error.bind(console));
</script>
<my-app>Loading...</my-app>

这里加载了此应用。 运行 ASP.NET 应用,你应该能看到一个 div 显示 "Loading..." 紧接着更新成显示 "Hello from TypeScript"。

Gulp

这篇快速上手指南将教你如何使用Gulp构建TypeScript,和如何在Gulp管道里添加BrowserifyuglifyWatchify。 本指南还会展示如何使用Babelify来添加Babel的功能。

这里假设你已经在使用Node.jsnpm了。

创建简单工程

我们首先创建一个新目录。 命名为proj,也可以使用任何你喜欢的名字。

mkdir proj
cd proj

我们将以下面的结构开始我们的工程:

proj/
   ├─ src/
   └─ dist/

TypeScript文件放在src文件夹下,经过TypeScript编译器编译生成的目标文件放在dist目录下。

下面让我们来创建这些文件夹:

mkdir src
mkdir dist

初始化工程

现在让我们把这个文件夹转换成npm包:

npm init

你将看到有一些提示操作。 除了入口文件外,其余的都可以使用默认项。 入口文件使用./dist/main.js。 你可以随时在package.json文件里更改生成的配置。

安装依赖项

现在我们可以使用npm install命令来安装包。 首先全局安装gulp-cli(如果你使用Unix系统,你可能需要在npm install命令上使用sudo)。

npm install -g gulp-cli

然后安装typescriptgulpgulp-typescript到开发依赖项。 Gulp-typescript是TypeScript的一个Gulp插件。

npm install --save-dev typescript gulp@4.0.0 gulp-typescript

写一个简单的例子

让我们写一个Hello World程序。 在src目录下创建main.ts文件:

function hello(compiler: string) {
    console.log(`Hello from ${compiler}`);
}
hello('TypeScript');

在工程的根目录proj下新建一个tsconfig.json文件:

{
    "files": [
        "src/main.ts"
    ],
    "compilerOptions": {
        "noImplicitAny": true,
        "target": "es5"
    }
}

新建gulpfile.js文件

在工程根目录下,新建一个gulpfile.js文件:

var gulp = require('gulp');
var ts = require('gulp-typescript');
var tsProject = ts.createProject('tsconfig.json');

gulp.task('default', function () {
    return tsProject.src()
        .pipe(tsProject())
        .js.pipe(gulp.dest('dist'));
});

测试这个应用

gulp
node dist/main.js

程序应该能够打印出“Hello from TypeScript!”。

向代码里添加模块

在使用Browserify前,让我们先构建一下代码然后再添加一些混入的模块。 这个结构将是你在真实应用程序中会用到的。

新建一个src/greet.ts文件:

export function sayHello(name: string) {
    return `Hello from ${name}`;
}

更改src/main.ts代码,从greet.ts导入sayHello

import { sayHello } from './greet';

console.log(sayHello('TypeScript'));

最后,将src/greet.ts添加到tsconfig.json

{
    "files": [
        "src/main.ts",
        "src/greet.ts"
    ],
    "compilerOptions": {
        "noImplicitAny": true,
        "target": "es5"
    }
}

确保执行gulp后模块是能工作的,在Node.js下进行测试:

gulp
node dist/main.js

注意,即使我们使用了ES2015的模块语法,TypeScript还是会生成Node.js使用的CommonJS模块。 我们在这个教程里会一直使用CommonJS模块,但是你可以通过修改module选项来改变这个行为。

Browserify

现在,让我们把这个工程由Node.js环境移到浏览器环境里。 因此,我们将把所有模块捆绑成一个JavaScript文件。 所幸,这正是Browserify要做的事情。 更方便的是,它支持Node.js的CommonJS模块,这也正是TypeScript默认生成的类型。 也就是说TypeScript和Node.js的设置不需要改变就可以移植到浏览器里。

首先,安装Browserify,tsify和vinyl-source-stream。 tsify是Browserify的一个插件,就像gulp-typescript一样,它能够访问TypeScript编译器。 vinyl-source-stream会将Browserify的输出文件适配成gulp能够解析的格式,它叫做vinyl

npm install --save-dev browserify tsify vinyl-source-stream

新建一个页面

src目录下新建一个index.html文件:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Hello World!</title>
    </head>
    <body>
        <p id="greeting">Loading ...</p>
        <script src="bundle.js"></script>
    </body>
</html>

修改main.ts文件来更新这个页面:

import { sayHello } from './greet';

function showHello(divName: string, name: string) {
    const elt = document.getElementById(divName);
    elt.innerText = sayHello(name);
}

showHello('greeting', 'TypeScript');

showHello调用sayHello函数更改页面上段落的文字。 现在修改gulpfile文件如下:

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var tsify = require('tsify');
var paths = {
    pages: ['src/*.html']
};

gulp.task('copy-html', function () {
    return gulp.src(paths.pages)
        .pipe(gulp.dest('dist'));
});

gulp.task('default', gulp.series(gulp.parallel('copy-html'), function () {
    return browserify({
        basedir: '.',
        debug: true,
        entries: ['src/main.ts'],
        cache: {},
        packageCache: {}
    })
    .plugin(tsify)
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('dist'));
}));

这里增加了copy-html任务并且把它加作default的依赖项。 这样,当default执行时,copy-html会被首先执行。 我们还修改了default任务,让它使用tsify插件调用Browserify,而不是gulp-typescript。 方便的是,两者传递相同的参数对象到TypeScript编译器。

调用bundle后,我们使用source(vinyl-source-stream的别名)把输出文件命名为bundle.js

测试此页面,运行gulp,然后在浏览器里打开dist/index.html。 你应该能在页面上看到“Hello from TypeScript”。

注意,我们为Broswerify指定了debug: true。 这会让tsify在输出文件里生成source mapssource maps允许我们在浏览器中直接调试TypeScript源码,而不是在合并后的JavaScript文件上调试。 你要打开调试器并在main.ts里打一个断点,看看source maps是否能工作。 当你刷新页面时,代码会停在断点处,从而你就能够调试greet.ts

Watchify,Babel和Uglify

现在代码已经用Browserify和tsify捆绑在一起了,我们可以使用Browserify插件为构建添加一些特性。

  • Watchify启动Gulp并保持运行状态,当你保存文件时自动编译。 帮你进入到编辑-保存-刷新浏览器的循环中。
  • Babel是个十分灵活的编译器,将ES2015及以上版本的代码转换成ES5和ES3。 你可以添加大量自定义的TypeScript目前不支持的转换器。
  • Uglify帮你压缩代码,将花费更少的时间去下载它们。

Watchify

我们启动Watchify,让它在后台帮我们编译:

npm install --save-dev watchify fancy-log

修改gulpfile文件如下:

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var watchify = require('watchify');
var tsify = require('tsify');
var fancy_log = require('fancy-log');
var paths = {
    pages: ['src/*.html']
};

var watchedBrowserify = watchify(browserify({
    basedir: '.',
    debug: true,
    entries: ['src/main.ts'],
    cache: {},
    packageCache: {}
}).plugin(tsify));

gulp.task('copy-html', function () {
    return gulp.src(paths.pages)
        .pipe(gulp.dest('dist'));
});

function bundle() {
    return watchedBrowserify
        .bundle()
        .on('error', fancy_log)
        .pipe(source('bundle.js'))
        .pipe(gulp.dest('dist'));
}

gulp.task('default', gulp.series(gulp.parallel('copy-html'), bundle));
watchedBrowserify.on('update', bundle);
watchedBrowserify.on('log', fancy_log);

共有三处改变,但是需要你略微重构一下代码。

  1. browserify实例包裹在watchify的调用里,控制生成的结果。
  2. 调用watchedBrowserify.on('update', bundle);,每次TypeScript文件改变时Browserify会执行bundle函数。
  3. 调用watchedBrowserify.on('log', fancy_log);将日志打印到控制台。

(1)和(2)在一起意味着我们要将browserify调用移出default任务。 然后给函数起个名字,因为Watchify和Gulp都要调用它。 (3)是可选的,但是对于调试来讲很有用。

现在当你执行gulp,它会启动并保持运行状态。 试着改变main.ts文件里showHello的代码并保存。 你会看到这样的输出:

proj$ gulp
[10:34:20] Using gulpfile ~/src/proj/gulpfile.js
[10:34:20] Starting 'copy-html'...
[10:34:20] Finished 'copy-html' after 26 ms
[10:34:20] Starting 'default'...
[10:34:21] 2824 bytes written (0.13 seconds)
[10:34:21] Finished 'default' after 1.36 s
[10:35:22] 2261 bytes written (0.02 seconds)
[10:35:24] 2808 bytes written (0.05 seconds)

Uglify

首先安装Uglify。 因为Uglify是用于混淆你的代码,所以我们还要安装vinyl-buffer和gulp-sourcemaps来支持sourcemaps。

npm install --save-dev gulp-uglify vinyl-buffer gulp-sourcemaps

修改gulpfile文件如下:

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var tsify = require('tsify');
var uglify = require('gulp-uglify');
var sourcemaps = require('gulp-sourcemaps');
var buffer = require('vinyl-buffer');
var paths = {
    pages: ['src/*.html']
};

gulp.task('copy-html', function () {
    return gulp.src(paths.pages)
        .pipe(gulp.dest('dist'));
});

gulp.task('default', gulp.series(gulp.parallel('copy-html'), function () {
    return browserify({
        basedir: '.',
        debug: true,
        entries: ['src/main.ts'],
        cache: {},
        packageCache: {}
    })
    .plugin(tsify)
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(uglify())
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('dist'));
}));

注意uglify只是调用了自己—buffersourcemaps的调用是用于确保sourcemaps可以工作。 这些调用让我们可以使用单独的sourcemap文件,而不是之前的内嵌的sourcemaps。 你现在可以执行gulp来检查bundle.js是否被压缩了:

gulp
cat dist/bundle.js

Babel

首先安装Babelify和ES2015的Babel预置程序。 和Uglify一样,Babelify也会混淆代码,因此我们也需要vinyl-buffer和gulp-sourcemaps。 默认情况下Babelify只会处理扩展名为.js.es.es6.jsx的文件,因此我们需要添加.ts扩展名到Babelify选项。

npm install --save-dev babelify@8 babel-core babel-preset-es2015 vinyl-buffer gulp-sourcemaps

修改gulpfile文件如下:

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var tsify = require('tsify');
var sourcemaps = require('gulp-sourcemaps');
var buffer = require('vinyl-buffer');
var paths = {
    pages: ['src/*.html']
};

gulp.task('copyHtml', function () {
    return gulp.src(paths.pages)
        .pipe(gulp.dest('dist'));
});

gulp.task('default', gulp.series(gulp.parallel('copy-html'), function () {
    return browserify({
        basedir: '.',
        debug: true,
        entries: ['src/main.ts'],
        cache: {},
        packageCache: {}
    })
    .plugin(tsify)
    .transform('babelify', {
        presets: ['es2015'],
        extensions: ['.ts']
    })
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('dist'));
}));

我们需要设置TypeScript目标为ES2015。 Babel稍后会从TypeScript生成的ES2015代码中生成ES5。 修改tsconfig.json:

{
    "files": [
        "src/main.ts"
    ],
    "compilerOptions": {
        "noImplicitAny": true,
        "target": "es2015"
    }
}

对于这样一段简单的代码来说,Babel的ES5输出应该和TypeScript的输出相似。

Knockout.js

注意: 此教程已从官方删除

这个快速上手指南会告诉你如何结合使用TypeScript和Knockout.js

这里我们假设你已经会使用Node.jsnpm

新建工程

首先,我们新建一个目录。 暂时命名为proj,当然了你可以使用任何喜欢的名字。

mkdir proj
cd proj

接下来,我们按如下方式来组织这个工程:

proj/
   ├─ src/
   └─ built/

TypeScript源码放在src目录下,结过TypeScript编译器编译后,生成的文件放在built目录里。

下面创建目录:

mkdir src
mkdir built

初始化工程

现在将这个文件夹转换为npm包。

npm init

你会看到一系列提示。 除了入口点外其它设置都可以使用默认值。 你可以随时到生成的package.json文件里修改这些设置。

安装构建依赖

首先确保TypeScript已经全局安装。

npm install -g typescript

我们还要获取Knockout的声明文件,它描述了这个库的结构供TypeScript使用。

npm install --save @types/knockout

获取运行时依赖

我们需要Knockout和RequireJS。 RequireJS是一个库,它可以让我们在运行时异步地加载模块。

有以下几种获取方式:

  1. 手动下载文件并维护它们。
  2. 通过像Bower这样的包管理下载并维护它们。
  3. 使用内容分发网络(CDN)来维护这两个文件。

我们使用第一种方法,它会简单一些,但是Knockout的官方文档上有讲解如何使用CDN,更多像RequireJS一样的代码库可以在cdnjs上查找。

下面让我们在工程根目录下创建externals目录。

mkdir externals

然后下载Knockout下载RequireJS到这个目录里。 最新的压缩后版本就可以。

添加TypeScript配置文件

下面我们想把所有的TypeScript文件整合到一起 - 包括自己写的和必须的声明文件。

我们需要创建一个tsconfig.json文件,包含了输入文件列表和编译选项。 在工程根目录下创建一个新文件tsconfig.json,内容如下:

{
    "compilerOptions": {
        "outDir": "./built/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "amd",
        "target": "es5"
    },
    "files": [
        "./src/require-config.ts",
        "./src/hello.ts"
    ]
}

这里引用了typings/index.d.ts,它是Typings帮我们创建的。 这个文件会自动地包含所有安装的依赖。

你可能会对typings目录下的browser.d.ts文件感到好奇,尤其因为我们将在浏览器里运行代码。 其实原因是这样的,当目标为浏览器的时候,一些包会生成不同的版本。 通常来讲,这些情况很少发生并且在这里我们不会遇到这种情况,所以我们可以忽略browser.d.ts

你可以在这里查看更多关于tsconfig.json文件的信息

写些代码

下面我们使用Knockout写一段TypeScript代码。 首先,在src目录里新建一个hello.ts文件。

import * as ko from "knockout";

class HelloViewModel {
    language: KnockoutObservable<string>
    framework: KnockoutObservable<string>

    constructor(language: string, framework: string) {
        this.language = ko.observable(language);
        this.framework = ko.observable(framework);
    }
}

ko.applyBindings(new HelloViewModel("TypeScript", "Knockout"));

接下来,在src目录下再新建一个require-config.ts文件。

declare var require: any;
require.config({
    paths: {
        "knockout": "externals/knockout-3.4.0",
    }
});

这个文件会告诉RequireJS从哪里导入Knockout,好比我们在hello.ts里做的一样。 你创建的所有页面都应该在RequireJS之后和导入任何东西之前引入它。 为了更好地理解这个文件和如何配置RequireJS,可以查看文档

我们还需要一个视图来显示HelloViewModel。 在proj目录的根上创建一个文件index.html,内容如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Hello Knockout!</title>
    </head>
    <body>
        <p>
            Hello from
            <strong data-bind="text: language">todo</strong>
            and
            <strong data-bind="text: framework">todo</strong>!
        </p>

        <p>Language: <input data-bind="value: language" /></p>
        <p>Framework: <input data-bind="value: framework" /></p>

        <script src="./externals/require.js"></script>
        <script src="./built/require-config.js"></script>
        <script>
            require(["built/hello"]);
        </script>
    </body>
</html>

注意,有两个script标签。 首先,我们引入RequireJS。 然后我们再在require-config.js里映射外部依赖,这样RequireJS就能知道到哪里去查找它们。 最后,使用我们要去加载的模块去调用require

将所有部分整合在一起

运行

tsc

现在,在你喜欢的浏览器打开index.html,所有都应该好用了。 你应该可以看到页面上显示“Hello from TypeScript and Knockout!”。 在它下面,你还会看到两个输入框。 当你改变输入和切换焦点时,就会看到原先显示的信息改变了。

React与webpack

这篇指南将会教你如何将TypeScript和React还有webpack结合在一起使用。

如果你正在做一个全新的工程,可以先阅读这篇React快速上手指南

否则,我们假设已经在使用Node.jsnpm

初始化项目结构

让我们新建一个目录。 将会命名为proj,但是你可以改成任何你喜欢的名字。

mkdir proj
cd proj

我们会像下面的结构组织我们的工程:

proj/
├─ dist/
└─ src/
   └─ components/

TypeScript文件会放在src文件夹里,通过TypeScript编译器编译,然后经webpack处理,最后生成一个main.js文件放在dist目录下。 我们自定义的组件将会放在src/components文件夹下。

下面来创建基本结构:

mkdir src
cd src
mkdir components
cd ..

Webpack会帮助我们生成dist目录。

初始化工程

现在把这个目录变成npm包。

npm init -y

它会使用默认值生成一个package.json文件。

安装依赖

首先确保已经全局安装了Webpack。

npm install --save-dev webpack webpack-cli

Webpack这个工具可以将你的所有代码和可选择地将依赖捆绑成一个单独的.js文件。

现在我们添加React和React-DOM以及它们的声明文件到package.json文件里做为依赖:

npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom

使用@types/前缀表示我们额外要获取React和React-DOM的声明文件。 通常当你导入像"react"这样的路径,它会查看react包; 然而,并不是所有的包都包含了声明文件,所以TypeScript还会查看@types/react包。 你会发现我们以后将不必在意这些。

接下来,我们要添加开发时依赖ts-loadersource-map-loader

npm install --save-dev typescript ts-loader source-map-loader

这些依赖会让TypeScript和webpack在一起良好地工作。 ts-loader可以让Webpack使用TypeScript的标准配置文件tsconfig.json编译TypeScript代码。 source-map-loader使用TypeScript输出的sourcemap文件来告诉webpack何时生成_自己的_sourcemaps。 这就允许你在调试最终生成的文件时就好像在调试TypeScript源码一样。

请注意,ts-loader并不是唯一的TypeScript加载器。

你还可以选择awesome-typescript-loader。 可以到这里查看它们之间的区别。

注意我们安装TypeScript为一个开发依赖。 我们还可以使用npm link typescript来链接TypeScript到一个全局拷贝,但这不是常见用法。

添加TypeScript配置文件

我们想将TypeScript文件整合到一起 - 这包括我们写的源码和必要的声明文件。

我们需要创建一个tsconfig.json文件,它包含了输入文件列表以及编译选项。 在工程根目录下新建文件tsconfig.json文件,添加以下内容:

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es6",
        "jsx": "react"
    }
}

你可以在这里了解更多关于tsconfig.json文件的说明。

写些代码

下面使用React写一段TypeScript代码。 首先,在src/components目录下创建一个名为Hello.tsx的文件,代码如下:

import * as React from "react";

export interface HelloProps { compiler: string; framework: string; }

export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;

注意这个例子使用了函数组件,我们可以让它更像一点_类_。

import * as React from "react";

export interface HelloProps { compiler: string; framework: string; }

// 'HelloProps' describes the shape of props.
// State is never set so we use the '{}' type.
export class Hello extends React.Component<HelloProps, {}> {
    render() {
        return <h1>Hello from {this.props.compiler} and {this.props.framework}!</h1>;
    }
}

接下来,在src下创建index.tsx文件,源码如下:

import * as React from "react";
import * as ReactDOM from "react-dom";

import { Hello } from "./components/Hello";

ReactDOM.render(
    <Hello compiler="TypeScript" framework="React" />,
    document.getElementById("example")
);

我们仅仅将Hello组件导入index.tsx。 注意,不同于"react""react-dom",我们使用Hello.tsx的_相对路径_ - 这很重要。 如果不这样做,TypeScript只会尝试在node_modules文件夹里查找。

我们还需要一个页面来显示Hello组件。 在根目录proj创建一个名为index.html的文件,如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Hello React!</title>
    </head>
    <body>
        <div id="example"></div>

        <!-- Dependencies -->
        <script src="./node_modules/react/umd/react.development.js"></script>
        <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

        <!-- Main -->
        <script src="./dist/main.js"></script>
    </body>
</html>

需要注意一点我们是从node_modules引入的文件。 React和React-DOM的npm包里包含了独立的.js文件,你可以在页面上引入它们,这里我们为了快捷就直接引用了。 可以随意地将它们拷贝到其它目录下,或者从CDN上引用。 Facebook在CND上提供了一系列可用的React版本,你可以在这里查看更多内容

创建一个webpack配置文件

在工程根目录下创建一个webpack.config.js文件。

module.exports = {
    mode: "production",

    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",

    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: [".ts", ".tsx"]
    },

    module: {
        rules: [
            {
                test: /\.ts(x?)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "ts-loader"
                    }
                ]
            },
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            {
                enforce: "pre",
                test: /\.js$/,
                loader: "source-map-loader"
            }
        ]
    },

    // When importing a module whose path matches one of the following, just
    // assume a corresponding global variable exists and use that instead.
    // This is important because it allows us to avoid bundling all of our
    // dependencies, which allows browsers to cache those libraries between builds.
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    }
};

大家可能对externals字段有所疑惑。 我们想要避免把所有的React都放到一个文件里,因为会增加编译时间并且浏览器还能够缓存没有发生改变的库文件。

理想情况下,我们只需要在浏览器里引入React模块,但是大部分浏览器还没有支持模块。 因此大部分代码库会把自己包裹在一个单独的全局变量内,比如:jQuery_。 这叫做“命名空间”模式,webpack也允许我们继续使用通过这种方式写的代码库。 通过我们的设置"react": "React",webpack会神奇地将所有对"react"的导入转换成从React全局变量中加载。

你可以在这里了解更多如何配置webpack。

整合在一起

执行:

npx webpack

在浏览器里打开index.html,工程应该已经可以用了! 你可以看到页面上显示着“Hello from TypeScript and React!”

React

这篇快速上手指南会教你如何将TypeScript与React结合起来使用。 在最后,你将学到:

  • 使用TypeScript和React创建工程
  • 使用TSLint进行代码检查
  • 使用JestEnzyme进行测试,以及
  • 使用Redux管理状态

我们会使用create-react-app工具快速搭建工程环境。

这里假设你已经在使用Node.jsnpm。 并且已经了解了React的基础知识

创建新工程

让我们首先创建一个叫做my-app的新工程:

npx create-react-app my-app --template typescript

react-scripts-ts是一系列适配器,它利用标准的create-react-app工程管道并把TypeScript混入进来。

此时的工程结构应如下所示:

my-app/
├─ .gitignore
├─ node_modules/
├─ public/
├─ src/
│  └─ ...
├─ package.json
├─ tsconfig.json
└─ tslint.json

注意:

  • tsconfig.json包含了工程里TypeScript特定的选项。
  • tslint.json保存了要使用的代码检查器的设置,TSLint
  • package.json包含了依赖,还有一些命令的快捷方式,如测试命令,预览命令和发布应用的命令。
  • public包含了静态资源如HTML页面或图片。除了index.html文件外,其它的文件都可以删除。
  • src包含了TypeScript和CSS源码。index.tsx是强制使用的入口文件。

运行工程

通过下面的方式即可轻松地运行这个工程。

npm run start

它会执行package.json里面指定的start命令,并且会启动一个服务器,当我们保存文件时还会自动刷新页面。 通常这个服务器的地址是http://localhost:3000,页面应用会被自动地打开。

它会保持监听以方便我们快速地预览改动。

测试工程

测试也仅仅是一行命令的事儿:

npm run test

这个命令会运行Jest,一个非常好用的测试工具,它会运行所有扩展名是.test.ts.spec.ts的文件。 好比是npm run start命令,当检测到有改动的时候Jest会自动地运行。 如果喜欢的话,你还可以同时运行npm run startnpm run test,这样你就可以在预览的同时进行测试。

生成生产环境的构建版本

在使用npm run start运行工程的时候,我们并没有生成一个优化过的版本。 通常我们想给用户一个运行的尽可能快并在体积上尽可能小的代码。 像压缩这样的优化方法可以做到这一点,但是总是要耗费更多的时间。 我们把这样的构建版本称做“生产环境”版本(与开发版本相对)。

要执行生产环境的构建,可以运行如下命令:

npm run build

这会相应地创建优化过的JS和CSS文件,./build/static/js./build/static/css

大多数情况下你不需要生成生产环境的构建版本, 但它可以帮助你衡量应用最终版本的体积大小。

创建一个组件

下面我们将要创建一个Hello组件。 这个组件接收任意一个我们想对之打招呼的名字(我们把它叫做name),并且有一个可选数量的感叹号做为结尾(通过enthusiasmLevel)。

若我们这样写<Hello name="Daniel" enthusiasmLevel={3} />,这个组件大至会渲染成<div>Hello Daniel!!!</div>。 如果没指定enthusiasmLevel,组件将默认显示一个感叹号。 若enthusiasmLevel0或负值将抛出一个错误。

下面来写一下Hello.tsx

// src/components/Hello.tsx

import * as React from 'react';

export interface Props {
  name: string;
  enthusiasmLevel?: number;
}

function Hello({ name, enthusiasmLevel = 1 }: Props) {
  if (enthusiasmLevel <= 0) {
    throw new Error('You could be a little more enthusiastic. :D');
  }

  return (
    <div className="hello">
      <div className="greeting">
        Hello {name + getExclamationMarks(enthusiasmLevel)}
      </div>
    </div>
  );
}

export default Hello;

// helpers

function getExclamationMarks(numChars: number) {
  return Array(numChars + 1).join('!');
}

注意我们定义了一个类型Props,它指定了我们组件要用到的属性。 name是必需的且为string类型,同时enthusiasmLevel是可选的且为number类型(你可以通过名字后面加?为指定可选参数)。

我们创建了一个函数组件Hello。 具体来讲,Hello是一个函数,接收一个Props对象并拆解它。 如果Props对象里没有设置enthusiasmLevel,默认值为1

使用函数是React中定义组件的两种方式之一。 如果你喜欢的话,也_可以_通过类的方式定义:

class Hello extends React.Component<Props, object> {
  render() {
    const { name, enthusiasmLevel = 1 } = this.props;

    if (enthusiasmLevel <= 0) {
      throw new Error('You could be a little more enthusiastic. :D');
    }

    return (
      <div className="hello">
        <div className="greeting">
          Hello {name + getExclamationMarks(enthusiasmLevel)}
        </div>
      </div>
    );
  }
}

当我们的组件具有某些状态的时候,使用类的方式是很有用处的。 但在这个例子里我们不需要考虑状态 - 事实上,在React.Component<Props, object>我们把状态指定为了object,因此使用函数组件更简洁。 当在创建可重用的通用UI组件的时候,在表现层使用组件局部状态比较适合。 针对我们应用的生命周期,我们会审视应用是如何通过Redux轻松地管理普通状态的。

现在我们已经写好了组件,让我们仔细看看index.tsx,把<App />替换成<Hello ... />

首先我们在文件头部导入它:

import Hello from './components/Hello';

然后修改render调用:

ReactDOM.render(
  <Hello name="TypeScript" enthusiasmLevel={10} />,
  document.getElementById('root') as HTMLElement
);

类型断言

这里还有一点要指出,就是最后一行document.getElementById('root') as HTMLElement。 这个语法叫做_类型断言_,有时也叫做_转换_。 当你比类型检查器更清楚一个表达式的类型的时候,你可以通过这种方式通知TypeScript。

这里,我们之所以这么做是因为getElementById的返回值类型是HTMLElement | null。 简单地说,getElementById返回null是当无法找对对应id元素的时候。 我们假设getElementById总是成功的,因此我们要使用as语法告诉TypeScript这点。

TypeScript还有一种感叹号(!)结尾的语法,它会从前面的表达式里移除nullundefined。 所以我们也_可以_写成document.getElementById('root')!,但在这里我们想写的更清楚些。

:sunglasses:添加样式

通过我们的设置为一个组件添加样式很容易。 若要设置Hello组件的样式,我们可以创建这样一个CSS文件src/components/Hello.css

.hello {
  text-align: center;
  margin: 20px;
  font-size: 48px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif
}

.hello button {
    margin-left: 25px;
    margin-right: 25px;
    font-size: 40px;
    min-width: 50px;
}

create-react-app包含的工具(Webpack和一些加载器)允许我们导入样式表文件。 当我们构建应用的时候,所有导入的.css文件会被拼接成一个输出文件。 因此在src/components/Hello.tsx,我们需要添加如下导入语句。

import './Hello.css';

使用Jest编写测试

如果你没使用过Jest,你可能先要把它安装为开发依赖项。

npm install -D jest jest-cli jest-config

我们对Hello组件有一些假设。 让我们在此重申一下:

  • 当这样写<Hello name="Daniel" enthusiasmLevel={3} />时,组件应被渲染成<div>Hello Daniel!!!</div>
  • 若未指定enthusiasmLevel,组件应默认显示一个感叹号。
  • enthusiasmLevel0或负值,它应抛出一个错误。

我们将针对这些需求为组件写一些注释。

但首先,我们要安装Enzyme。 Enzyme是React生态系统里一个通用工具,它方便了针对组件的行为编写测试。 默认地,我们的应用包含了一个叫做jsdom的库,它允许我们模拟DOM以及在非浏览器的环境下测试运行时的行为。 Enzyme与此类似,但是是基于jsdom的,并且方便我们查询组件。

让我们把它安装为开发依赖项。

npm install -D enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16

如果你的react版本低于15.5.0,还需安装如下

npm install -D react-addons-test-utils

注意我们同时安装了enzyme@types/enzymeenzyme包指的是包含了实际运行的JavaScript代码包,而@types/enzyme则包含了声明文件(.d.ts文件)的包,以便TypeScript能够了解该如何使用Enzyme。 你可以在这里了解更多关于@types包的信息。

我们还需要安装enzyme-adapterreact-addons-test-utils。 它们是使用enzyme所需要安装的包,前者作为配置适配器是必须的,而后者若采用的React版本在15.5.0之上则毋需安装。

现在我们已经设置好了Enzyme,下面开始编写测试! 先创建一个文件src/components/Hello.test.tsx,与先前的Hello.tsx文件放在一起。

// src/components/Hello.test.tsx

import * as React from 'react';
import * as enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import Hello from './Hello';

enzyme.configure({ adapter: new Adapter() });

it('renders the correct text when no enthusiasm level is given', () => {
  const hello = enzyme.shallow(<Hello name='Daniel' />);
  expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
});

it('renders the correct text with an explicit enthusiasm of 1', () => {
  const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={1}/>);
  expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
});

it('renders the correct text with an explicit enthusiasm level of 5', () => {
  const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={5} />);
  expect(hello.find(".greeting").text()).toEqual('Hello Daniel!!!!!');
});

it('throws when the enthusiasm level is 0', () => {
  expect(() => {
    enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={0} />);
  }).toThrow();
});

it('throws when the enthusiasm level is negative', () => {
  expect(() => {
    enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={-1} />);
  }).toThrow();
});

这些测试都十分基础,但你可以从中得到启发。

添加state管理

到此为止,如果你使用React的目的是只获取一次数据并显示,那么你已经完成了。 但是如果你想开发一个可以交互的应用,那么你需要添加state管理。

state管理概述

React本身就是一个适合于创建可组合型视图的库。 但是,React并没有任何在应用间同步数据的功能。 就React组件而言,数据是通过每个元素上指定的props向子元素传递。

因为React本身并没有提供内置的state管理功能,React社区选择了Redux和MobX库。

Redux依靠一个统一且不可变的数据存储来同步数据,并且更新那里的数据时会触发应用的更新渲染。 state的更新是以一种不可变的方式进行,它会发布一条明确的action消息,这个消息必须被reducer函数处理。 由于使用了这样明确的方式,很容易弄清楚一个action是如何影响程序的state。

MobX借助于函数式响应型模式,state被包装在了可观察对象里,并通过props传递。 通过将state标记为可观察的,即可在所有观察者之间保持state的同步性。 另一个好处是,这个库已经使用TypeScript实现了。

这两者各有优缺点。 但Redux使用得更广泛,因此在这篇教程里,我们主要看如何使用Redux; 但是也鼓励大家两者都去了解一下。

后面的小节学习曲线比较陡。 因此强烈建议大家先去熟悉一下Redux

设置actions

只有当应用里的state会改变的时候,我们才需要去添加Redux。 我们需要一个action的来源,它将触发改变。 它可以是一个定时器或者UI上的一个按钮。

为此,我们将增加两个按钮来控制Hello组件的感叹级别。

安装Redux

安装reduxreact-redux以及它们的类型文件做为依赖。

npm install -S redux react-redux @types/react-redux

这里我们不需要安装@types/redux,因为Redux已经自带了声明文件(.d.ts文件)。

定义应用的状态

我们需要定义Redux保存的state的结构。 创建src/types/index.tsx文件,它保存了类型的定义,我们在整个程序里都可能用到。

// src/types/index.tsx

export interface StoreState {
    languageName: string;
    enthusiasmLevel: number;
}

这里我们想让languageName表示应用使用的编程语言(例如,TypeScript或者JavaScript),enthusiasmLevel是可变的。 在写我们的第一个容器的时候,就会明白为什么要令state与props稍有不同。

添加actions

下面我们创建这个应用将要响应的消息类型,src/constants/index.tsx

// src/constants/index.tsx

export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;


export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;

这里的const/type模式允许我们以容易访问和重构的方式使用TypeScript的字符串字面量类型。

接下来,我们创建一些actions以及创建这些actions的函数,src/actions/index.tsx

import * as constants from '../constants'

export interface IncrementEnthusiasm {
    type: constants.INCREMENT_ENTHUSIASM;
}

export interface DecrementEnthusiasm {
    type: constants.DECREMENT_ENTHUSIASM;
}

export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;

export function incrementEnthusiasm(): IncrementEnthusiasm {
    return {
        type: constants.INCREMENT_ENTHUSIASM
    }
}

export function decrementEnthusiasm(): DecrementEnthusiasm {
    return {
        type: constants.DECREMENT_ENTHUSIASM
    }
}

我们创建了两个类型,它们负责增加操作和减少操作的行为。 我们还定义了一个类型(EnthusiasmAction),它描述了哪些action是可以增加或减少的。 最后,我们定义了两个函数用来创建实际的actions。

这里有一些清晰的模版,你可以参考类似redux-actions的库。

添加reducer

现在我们可以开始写第一个reducer了! Reducers是函数,它们负责生成应用state的拷贝使之产生变化,但它并没有_副作用_。 它们是一种纯函数

我们的reducer将放在src/reducers/index.tsx文件里。 它的功能是保证增加操作会让感叹级别加1,减少操作则要将感叹级别减1,但是这个级别永远不能小于1。

// src/reducers/index.tsx

import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';

export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
  switch (action.type) {
    case INCREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
    case DECREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
  }
  return state;
}

注意我们使用了_对象展开_(...state),当替换enthusiasmLevel时,它可以对状态进行浅拷贝。 将enthusiasmLevel属性放在末尾是十分关键的,否则它将被旧的状态覆盖。

你可能想要对reducer写一些测试。 因为reducers是纯函数,它们可以传入任意的数据。 针对每个输入,可以测试reducers生成的新的状态。 可以考虑使用Jest的toEqual方法。

创建容器

在使用Redux时,我们常常要创建组件和容器。 组件是数据无关的,且工作在表现层。 _容器_通常包裹组件及其使用的数据,用以显示和修改状态。 你可以在这里阅读更多关于这个概念的细节:Dan Abramov写的_表现层的容器组件_

现在我们修改src/components/Hello.tsx,让它可以修改状态。 我们将添加两个可选的回调属性到Props,它们分别是onIncrementonDecrement

export interface Props {
  name: string;
  enthusiasmLevel?: number;
  onIncrement?: () => void;
  onDecrement?: () => void;
}

然后将这两个回调绑定到两个新按钮上,将按钮添加到我们的组件里。

function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) {
  if (enthusiasmLevel <= 0) {
    throw new Error('You could be a little more enthusiastic. :D');
  }

  return (
    <div className="hello">
      <div className="greeting">
        Hello {name + getExclamationMarks(enthusiasmLevel)}
      </div>
      <div>
        <button onClick={onDecrement}>-</button>
        <button onClick={onIncrement}>+</button>
      </div>
    </div>
  );
}

通常情况下,我们应该给onIncrementonDecrement写一些测试,它们是在各自的按钮被点击时调用。 试一试以便掌握编写测试的窍门。

现在我们的组件更新好了,可以把它放在一个容器里了。 让我们来创建一个文件src/containers/Hello.tsx,在开始的地方使用下列导入语句。

import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';

两个关键点是初始的Hello组件和react-redux的connect函数。 connect可以将我们的Hello组件转换成一个容器,通过以下两个函数:

  • mapStateToProps将当前store里的数据以我们的组件需要的形式传递到组件。
  • mapDispatchToProps利用dispatch函数,创建回调props将actions送到store。

回想一下,我们的应用包含两个属性:languageNameenthusiasmLevel。 我们的Hello组件,希望得到一个name和一个enthusiasmLevelmapStateToProps会从store得到相应的数据,如果需要的话将针对组件的props调整它。 下面让我们继续往下写。

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}

注意mapStateToProps仅创建了Hello组件需要的四个属性中的两个。 我们还想要传入onIncrementonDecrement回调函数。 mapDispatchToProps是一个函数,它需要传入一个调度函数。 这个调度函数可以将actions传入store来触发更新,因此我们可以创建一对回调函数,它们会在需要的时候调用调度函数。

export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  return {
    onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    onDecrement: () => dispatch(actions.decrementEnthusiasm()),
  }
}

最后,我们可以调用connect了。 connect首先会接收mapStateToPropsmapDispatchToProps,然后返回另一个函数,我们用它来包裹我们的组件。 最终的容器是通过下面的代码定义的:

export default connect(mapStateToProps, mapDispatchToProps)(Hello);

现在,我们的文件应该是下面这个样子:

// src/containers/Hello.tsx

import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}

export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  return {
    onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    onDecrement: () => dispatch(actions.decrementEnthusiasm()),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Hello);

创建store

让我们回到src/index.tsx。 要把所有的东西合到一起,我们需要创建一个带初始状态的store,并用我们所有的reducers来设置它。

import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';

const store = createStore<StoreState>(enthusiasm, {
  enthusiasmLevel: 1,
  languageName: 'TypeScript',
});

store可能正如你想的那样,它是我们应用全局状态的核心store。

接下来,我们将要用./src/containers/Hello来包裹./src/components/Hello,然后使用react-redux的Provider将props与容器连通起来。 我们将导入它们:

import Hello from './containers/Hello';
import { Provider } from 'react-redux';

storeProvider的属性形式传入:

ReactDOM.render(
  <Provider store={store}>
    <Hello />
  </Provider>,
  document.getElementById('root') as HTMLElement
);

注意,Hello不再需要props了,因为我们使用了connect函数为包裹起来的Hello组件的props适配了应用的状态。

退出

如果你发现create-react-app使一些自定义设置变得困难,那么你就可以选择不使用它,使用你需要配置。 比如,你要添加一个Webpack插件,你就可以利用create-react-app提供的“eject”功能。

运行:

npm run eject

这样就可以了!

你要注意,在运行eject前最好保存你的代码。 你不能撤销eject命令,因此退出操作是永久性的除非你从一个运行eject前的提交来恢复工程。

下一步

create-react-app带有很多很棒的功能。 它们的大多数都在我们工程生成的README.md里面有记录,所以可以简单阅读一下。

如果你想学习更多关于Redux的知识,你可以前往官方站点查看文档。 同样的,MobX官方站点。

如果你想要在某个时间点eject,你需要了解再多关于Webpack的知识。 你可以查看React & Webpack教程

有时候你需要路由功能。 已经有一些解决方案了,但是对于Redux工程来讲react-router是最流行的,并经常与react-router-redux联合使用。

Angular 2

即将到来的Angular 2框架是使用TypeScript开发的。 因此Angular和TypeScript一起使用非常简单方便。 Angular团队也在其文档里把TypeScript视为一等公民。

正因为这样,你总是可以在Angular 2官网(或Angular 2官网中文版)里查看到最新的结合使用Angular和TypeScript的参考文档。 在这里查看快速上手指南,现在就开始学习吧!

从JavaScript迁移到TypeScript

TypeScript不是凭空存在的。 它从JavaScript生态系统和大量现存的JavaScript而来。 将JavaScript代码转换成TypeScript虽乏味却不是难事。 接下来这篇教程将教你怎么做。 在开始转换TypeScript之前,我们假设你已经理解了足够多本手册里的内容。

如果你打算要转换一个React工程,推荐你先阅读React转换指南

设置目录

如果你在写纯JavaScript,你大概是想直接运行这些JavaScript文件, 这些文件存在于srclibdist目录里,它们可以按照预想运行。

若如此,那么你写的纯JavaScript文件将做为TypeScript的输入,你将要运行的是TypeScript的输出。 在从JS到TS的转换过程中,我们会分离输入文件以防TypeScript覆盖它们。 你也可以指定输出目录。

你可能还需要对JavaScript做一些中间处理,比如合并或经过Babel再次编译。 在这种情况下,你应该已经有了如下的目录结构。

那么现在,我们假设你已经设置了这样的目录结构:

projectRoot
├── src
│   ├── file1.js
│   └── file2.js
├── built
└── tsconfig.json

如果你在src目录外还有tests文件夹,那么在src里可以有一个tsconfig.json文件,在tests里还可以有一个。

书写配置文件

TypeScript使用tsconfig.json文件管理工程配置,例如你想包含哪些文件和进行哪些检查。 让我们先创建一个简单的工程配置文件:

{
    "compilerOptions": {
        "outDir": "./built",
        "allowJs": true,
        "target": "es5"
    },
    "include": [
        "./src/**/*"
    ]
}

这里我们为TypeScript设置了一些东西:

  1. 读取所有可识别的src目录下的文件(通过include)。
  2. 接受JavaScript做为输入(通过allowJs)。
  3. 生成的所有文件放在built目录下(通过outDir)。
  4. 将JavaScript代码降级到低版本比如ECMAScript 5(通过target)。

现在,如果你在工程根目录下运行tsc,就可以在built目录下看到生成的文件。 built下的文件应该与src下的文件相同。 现在你的工程里的TypeScript已经可以工作了。

早期收益

现在你已经可以看到TypeScript带来的好处,它能帮助我们理解当前工程。 如果你打开像VS CodeVisual Studio这样的编译器,你就能使用像自动补全这样的工具。 你还可以配置如下的选项来帮助查找BUG:

  • noImplicitReturns 会防止你忘记在函数末尾返回值。
  • noFallthroughCasesInSwitch 会防止在switch代码块里的两个case之间忘记添加break语句。

TypeScript还能发现那些执行不到的代码和标签,你可以通过设置allowUnreachableCodeallowUnusedLabels选项来禁用。

与构建工具进行集成

在你的构建管道中可能包含多个步骤。 比如为每个文件添加一些内容。 每种工具的使用方法都是不同的,我们会尽可能的包涵主流的工具。

Gulp

如果你在使用时髦的Gulp,我们已经有一篇关于使用Gulp结合TypeScript并与常见构建工具Browserify,Babelify和Uglify进行集成的教程。 请阅读这篇教程。

Webpack

Webpack集成非常简单。 你可以使用awesome-typescript-loader,它是一个TypeScript的加载器,结合source-map-loader方便调试。 运行:

npm install awesome-typescript-loader source-map-loader

并将下面的选项合并到你的webpack.config.js文件里:

module.exports = {
    entry: "./src/index.ts",
    output: {
        filename: "./dist/bundle.js",
    },

    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",

    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
    },

    module: {
        loaders: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" }
        ],

        preLoaders: [
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { test: /\.js$/, loader: "source-map-loader" }
        ]
    },

    // Other options...
};

要注意的是,awesome-typescript-loader必须在其它处理.js文件的加载器之前运行。

这与另一个TypeScript的Webpack加载器ts-loader是一样的。 你可以到这里了解两者之间的差别。

你可以在React和Webpack教程里找到使用Webpack的例子。

转换到TypeScript文件

到目前为止,你已经做好了使用TypeScript文件的准备。 第一步,将.js文件重命名为.ts文件。 如果你使用了JSX,则重命名为.tsx文件。

第一步达成? 太棒了! 你已经成功地将一个文件从JavaScript转换成了TypeScript!

当然了,你可能感觉哪里不对劲儿。 如果你在支持TypeScript的编辑器(或运行tsc --pretty)里打开了那个文件,你可能会看到有些行上有红色的波浪线。 你可以把它们当做在Microsoft Word里看到的红色波浪线一样。 但是TypeScript仍然会编译你的代码,就好比Word还是允许你打印你的文档一样。

如果对你来说这种行为太随便了,你可以让它变得严格些。 如果,你_不想_在发生错误的时候,TypeScript还会被编译成JavaScript,你可以使用noEmitOnError选项。 从某种意义上来讲,TypeScript具有一个调整它的严格性的刻度盘,你可以将指针拔动到你想要的位置。

如果你计划使用可用的高度严格的设置,最好现在就启用它们(查看启用严格检查)。 比如,如果你不想让TypeScript将没有明确指定的类型默默地推断为any类型,可以在修改文件之前启用noImplicitAny。 你可能会觉得这有些过度严格,但是长期收益很快就能显现出来。

去除错误

我们提到过,若不出所料,在转换后将会看到错误信息。 重要的是我们要逐一的查看它们并决定如何处理。 通常这些都是真正的BUG,但有时必须要告诉TypeScript你要做的是什么。

由模块导入

首先你可能会看到一些类似Cannot find name 'require'.Cannot find name 'define'.的错误。 遇到这种情况说明你在使用模块。 你仅需要告诉TypeScript它们是存在的:

// For Node/CommonJS
declare function require(path: string): any;

// For RequireJS/AMD
declare function define(...args: any[]): any;

最好是避免使用这些调用而改用TypeScript的导入语法。

首先,你要使用TypeScript的module标记来启用一些模块系统。 可用的选项有commonjsamdsystem,and umd

如果代码里存在下面的Node/CommonJS代码:

var foo = require("foo");

foo.doStuff();

或者下面的RequireJS/AMD代码:

define(["foo"], function(foo) {
    foo.doStuff();
})

那么可以写做下面的TypeScript代码:

import foo = require("foo");

foo.doStuff();

获取声明文件

如果你开始做转换到TypeScript导入,你可能会遇到Cannot find module 'foo'.这样的错误。 问题出在没有_声明文件_来描述你的代码库。 幸运的是这非常简单。 如果TypeScript报怨像是没有lodash包,那你只需这样做

npm install -S @types/lodash

如果你没有使用commonjs模块模块选项,那么就需要将moduleResolution选项设置为node

之后,你应该就可以导入lodash了,并且会获得精确的自动补全功能。

由模块导出

通常来讲,由模块导出涉及添加属性到exportsmodule.exports。 TypeScript允许你使用顶级的导出语句。 比如,你要导出下面的函数:

module.exports.feedPets = function(pets) {
    // ...
}

那么你可以这样写:

export function feedPets(pets) {
    // ...
}

有时你会完全重写导出对象。 这是一个常见模式,这会将模块变为可立即调用的模块:

var express = require("express");
var app = express();

之前你可以是这样写的:

function foo() {
    // ...
}
module.exports = foo;

在TypeScript里,你可以使用export =来代替。

function foo() {
    // ...
}
export = foo;

过多或过少的参数

有时你会发现你在调用一个具有过多或过少参数的函数。 通常,这是一个BUG,但在某些情况下,你可以声明一个使用arguments对象的函数而不需要写出所有参数:

function myCoolFunction() {
    if (arguments.length == 2 && !Array.isArray(arguments[1])) {
        var f = arguments[0];
        var arr = arguments[1];
        // ...
    }
    // ...
}

myCoolFunction(function(x) { console.log(x) }, [1, 2, 3, 4]);
myCoolFunction(function(x) { console.log(x) }, 1, 2, 3, 4);

这种情况下,我们需要利用TypeScript的函数重载来告诉调用者myCoolFunction函数的调用方式。

function myCoolFunction(f: (x: number) => void, nums: number[]): void;
function myCoolFunction(f: (x: number) => void, ...nums: number[]): void;
function myCoolFunction() {
    if (arguments.length == 2 && !Array.isArray(arguments[1])) {
        var f = arguments[0];
        var arr = arguments[1];
        // ...
    }
    // ...
}

我们为myCoolFunction函数添加了两个重载签名。 第一个检查myCoolFunction函数是否接收一个函数(它又接收一个number参数)和一个number数组。 第二个同样是接收了一个函数,并且使用剩余参数(...nums)来表示之后的其它所有参数必须是number类型。

连续添加属性

有些人可能会因为代码美观性而喜欢先创建一个对象然后立即添加属性:

var options = {};
options.color = "red";
options.volume = 11;

TypeScript会提示你不能给colorvolumn赋值,因为先前指定options的类型为{}并不带有任何属性。 如果你将声明变成对象字面量的形式将不会产生错误:

let options = {
    color: "red",
    volume: 11
};

你还可以定义options的类型并且添加类型断言到对象字面量上。

interface Options { color: string; volume: number }

let options = {} as Options;
options.color = "red";
options.volume = 11;

或者,你可以将options指定成any类型,这是最简单的,但也是获益最少的。

anyObject,和{}

你可能会试图使用Object{}来表示一个值可以具有任意属性,因为Object是最通用的类型。 然而在这种情况下**any是真正想要使用的类型**,因为它是最_灵活_的类型。

比如,有一个Object类型的东西,你将不能够在其上调用toLowerCase()

越普通意味着更少的利用类型,但是any比较特殊,它是最普通的类型但是允许你在上面做任何事情。 也就是说你可以在上面调用,构造它,访问它的属性等等。 记住,当你使用any时,你会失去大多数TypeScript提供的错误检查和编译器支持。

如果你还是决定使用Object{},你应该选择{}。 虽说它们基本一样,但是从技术角度上来讲{}在一些深奥的情况里比Object更普通。

启用严格检查

TypeScript提供了一些检查来保证安全以及帮助分析你的程序。 当你将代码转换为了TypeScript后,你可以启用这些检查来帮助你获得高度安全性。

没有隐式的any

在某些情况下TypeScript没法确定某些值的类型。 那么TypeScript会使用any类型代替。 这对代码转换来讲是不错,但是使用any意味着失去了类型安全保障,并且你得不到工具的支持。 你可以使用noImplicitAny选项,让TypeScript标记出发生这种情况的地方,并给出一个错误。

严格的nullundefined检查

默认地,TypeScript把nullundefined当做属于任何类型。 这就是说,声明为number类型的值可以为nullundefined。 因为在JavaScript和TypeScript里,nullundefined经常会导致BUG的产生,所以TypeScript包含了strictNullChecks选项来帮助我们减少对这种情况的担忧。

当启用了strictNullChecksnullundefined获得了它们自己各自的类型nullundefined。 当任何值_可能_为null,你可以使用联合类型。 比如,某值可能为numbernull,你可以声明它的类型为number | null

假设有一个值TypeScript认为可以为nullundefined,但是你更清楚它的类型,你可以使用!后缀。

declare var foo: string[] | null;

foo.length;  // error - 'foo' is possibly 'null'

foo!.length; // okay - 'foo!' just has type 'string[]'

要当心,当你使用strictNullChecks,你的依赖也需要相应地启用strictNullChecks

this没有隐式的any

当你在类的外部使用this关键字时,它会默认获得any类型。 比如,假设有一个Point类,并且我们要添加一个函数做为它的方法:

class Point {
    constructor(public x, public y) {}
    getDistance(p: Point) {
        let dx = p.x - this.x;
        let dy = p.y - this.y;
        return Math.sqrt(dx ** 2 + dy ** 2);
    }
}
// ...

// Reopen the interface.
interface Point {
    distanceFromOrigin(point: Point): number;
}
Point.prototype.distanceFromOrigin = function(point: Point) {
    return this.getDistance({ x: 0, y: 0});
}

这就产生了我们上面提到的错误 - 如果我们错误地拼写了getDistance并不会得到一个错误。 正因此,TypeScript有noImplicitThis选项。 当设置了它,TypeScript会产生一个错误当没有明确指定类型(或通过类型推断)的this被使用时。 解决的方法是在接口或函数上使用指定了类型的this参数:

Point.prototype.distanceFromOrigin = function(this: Point, point: Point) {
    return this.getDistance({ x: 0, y: 0});
}

手册

基础类型

介绍

为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

Boolean

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

let isDone: boolean = false;

Number

和JavaScript一样,TypeScript里的所有数字都是浮点数或者大整数 。 这些浮点数的类型是number, 而大整数的类型则是 bigint。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
let bigLiteral: bigint = 100n;

String

JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用string表示文本数据类型。 和JavaScript一样,可以使用双引号(")或单引号(')表示字符串。

let name: string = "bob";
name = "smith";

你还可以使用_模版字符串_,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围(` `),并且以${ expr }这种形式嵌入表达式

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;

这与下面定义sentence的方式效果相同:

let sentence: string = "Hello, my name is " + name + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

Array

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:

let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>

let list: Array<number> = [1, 2, 3];

Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,你可以定义一对值分别为stringnumber类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素会报错。

x[3] = "world"; // Error, Property '3' does not exist on type '[string, number]'.

console.log(x[5].toString()); // Error, Property '5' does not exist on type '[string, number]'.

Enum

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从1开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

Unknown

当我们在写应用的时候可能会需要描述一个我们还不知道其类型的变量。这些值可以来自动态内容,例如从用户获得,或者我们想在我们的 API 中接收所有可能类型的值。在这些情况下,我们想要让编译器以及未来的用户知道这个变量可以是任意类型。这个时候我们会对它使用 unknown 类型。

let notSure: unknown = 4;
notSure = "maybe a string instead";

// OK, definitely a boolean
notSure = false;

如果你有一个 unknwon 类型的变量,你可以通过进行 typeof 、比较或者更高级的类型检查来将其的类型范围缩小,这些方法会在后续章节中进一步讨论:

// @errors: 2322 2322 2322
declare const maybe: unknown;
// 'maybe' could be a string, object, boolean, undefined, or other types
const aNumber: number = maybe;

if (maybe === true) {
  // TypeScript knows that maybe is a boolean now
  const aBoolean: boolean = maybe;
  // So, it cannot be a string
  const aString: string = maybe;
}

if (typeof maybe === "string") {
  // TypeScript knows that maybe is a string
  const aString: string = maybe;
  // So, it cannot be a boolean
  const aBoolean: boolean = maybe;
}

Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用any类型来标记这些变量:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为Object有相似的作用,就像它在其它语言中那样。 但是Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

注意:应避免使用Object,而是使用非原始object类型,正如Do's and Don'ts里所讲的那样。

当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;

Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是void

function warnUser(): void {
    console.log("This is my warning message");
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予null(只在--strictNullChecks未指定时)和undefined

let unusable: void = undefined;

Null 和 Undefined

TypeScript里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和void相似,它们的本身的类型用处不是很大:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给any和它们各自的类型(有一个例外是undefined还可以赋值给void类型)。 这能避免_很多_常见的问题。 也许在某处你想传入一个stringnullundefined,你可以使用联合类型string | null | undefined

联合类型是高级主题,我们会在以后的章节里讨论它。

注意:我们鼓励尽可能地使用--strictNullChecks,但在本手册里我们假设这个标记是关闭的。

Never

never类型表示的是那些永不存在的值的类型。 例如,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,_没有_类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使any也不可以赋值给never

下面是一些返回never类型的函数:

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

Object

object表示非原始类型,也就是除numberstringbooleanbigintsymbolnullundefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

类型断言

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过_类型断言_这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有as语法断言是被允许的。

关于let

你可能已经注意到了,我们使用let关键字来代替大家所熟悉的JavaScript关键字varlet是ES2015引入的关键字,它比var更加安全,因此被看做是声明变量的标准方式。 我们会在以后详细介绍它,很多常见的问题都可以通过使用let来解决,所以尽可能地使用let来代替var吧。

关于 Number, String, Boolean, Symbol 和 Object

我们很容易会认为 NumberStringBooleanSymbol 以及 Object 这些类型和我们以上推荐的小写版本的类型是一样的。但这些类型不属于语言的基本类型,并且几乎在任何时候都不应该被用作一个类型:

// @errors: 2339
function reverse(s: String): String {
  return s.split("").reverse().join("");
}

reverse("hello world");

相对地,我们应该使用 numberstringbooleanobjectsymbol

function reverse(s: string): string {
  return s.split("").reverse().join("");
}

reverse("hello world");

接口

介绍

TypeScript 的核心原则之一是对值所具有的_结构_进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口初探

下面通过一个简单示例来观察接口是如何工作的:

function printLabel(labeledObj: { label: string }) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候 TypeScript 却并不会这么宽松,我们下面会稍做讲解。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

LabeledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

下面是应用了“option bags”的例子:

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: "black" });

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将createSquare里的color属性名拼错,就会得到一个错误提示:

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.clor) {
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: "black" });

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用readonly来指定只读属性:

interface Point {
  readonly x: number;
  readonly y: number;
}

你可以通过赋值一个对象字面量来构造一个Point。 赋值后,xy再也不能被改变了。

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript 具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

a = ro as number[];

readonly vs const

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const,若做为属性则使用readonly

额外的属性检查

我们在第一个例子里使用了接口,TypeScript 让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。

然而,天真地将这两者结合的话就会像在 JavaScript 里那样搬起石头砸自己的脚。比如,拿createSquare例子来说:

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  // ...
}

let mySquare = createSquare({ colour: "red", width: 100 });

注意传入createSquare的参数拼写为colour而不是color。 在 JavaScript 里,这会默默地失败。

你可能会争辩这个程序已经正确地类型化了,因为width属性是兼容的,不存在color属性,而且额外的colour属性是无意义的。

然而,TypeScript 会认为这段代码可能存在 bug。 对象字面量会被特殊对待而且会经过_额外属性检查_,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

// error: Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
let mySquare = createSquare({ colour: "red", width: 100 });

绕开这些检查非常简单。 最简便的方法是使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果SquareConfig带有上面定义的类型的colorwidth属性,并且_还会_带有任意数量的其它属性,那么我们可以这样定义它:

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是colorwidth,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为squareOptions不会经过额外属性检查,所以编译器不会报错。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

上面的方法只在squareOptionsSquareConfig之间有共同的属性时才好用。 在这个例子中,这个属性为width。如果变量间不存在共同的对象属性将会报错。例如:

let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);

要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的 bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入colorcolour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript 的类型系统会推断出参数类型,因为函数直接赋值给了SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是falsetrue)。

let mySearch: SearchFunc;
mySearch = function(src, sub) {
  let result = src.search(sub);
  return result > -1;
};

如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc接口中的定义不匹配。

let mySearch: SearchFunc;

// error: Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'.
// Type 'string' is not assignable to type 'boolean'.
mySearch = function(src, sub) {
  let result = src.search(sub);
  return "string";
};

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]ageMap["daniel"]。 可索引类型具有一个_索引签名_,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

上面例子里,我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用number去索引StringArray时会得到string类型的返回值。

Typescript 支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用number来索引时,JavaScript 会将它转换成string然后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

class Animal {
  name: string;
}
class Dog extends Animal {
  breed: string;
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了obj.propertyobj["property"]两种形式都可以。 下面的例子里,name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

interface NumberDictionary {
  [index: string]: number;
  length: number; // 可以,length是number类型
  name: string; // 错误,`name`的类型与索引类型返回值的类型不匹配
}

但如果索引签名是包含属性类型的联合类型,那么使用不同类型的属性就是允许的。

interface NumberOrStringDictionary {
   [index: string]: number | string;
   length: number;    // ok, length is a number
   name: string;      // ok, name is a string
}

最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

你不能设置myArray[2],因为索引签名是只读的。

类类型

实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

interface ClockInterface {
  currentTime: Date;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  constructor(h: number, m: number) {}
}

你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

类静态部分与实例部分的区别

当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:

interface ClockConstructor {
  new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
  currentTime: Date;
  constructor(h: number, m: number) {}
}

这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor 存在于类的静态部分,所以不在检查的范围内。

因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口,ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数createClock,它用传入的类型创建实例。

interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
  tick(): void;
}

function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): ClockInterface {
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}
class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("tick tock");
  }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

另一种简单方式是使用类表达式:

interface ClockConstructor {
  new (hour: number, minute: number);
}

interface ClockInterface {
  tick();
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
};

继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

先前我们提过,接口能够描述 JavaScript 里丰富的类型。 因为 JavaScript 其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

一个例子就是,一个对象可以同时作为函数和对象使用,并带有额外的属性。

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = function(start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function() {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

在使用 JavaScript 第三方库的时候,你可能需要像上面那样去完整地定义类型。

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的 private 和 protected 成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 除了继承自基类,子类之间不必相关联。 例:

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() {}
}

class TextBox extends Control {
  select() {}
}

class ImageControl implements SelectableControl {
// Error: Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
//  Types have separate declarations of a private property 'state'.
  private state: any;
  select() {}
}

在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。

Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上,SelectableControl就像Control一样,并拥有一个select方法。 ButtonTextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法)。而对于 ImageControl 类,它有自身的私有成员 state 而不是通过继承 Control 得来的,所以它不可以实现 SelectableControl

函数

介绍

函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义_行为_的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。

函数

和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。

通过下面的例子可以迅速回想起这两种JavaScript中的函数:

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,我们说它‘捕获’了这些变量。 至于为什么可以这样做以及其中的利弊超出了本文的范围,但是深刻理解这个机制对学习JavaScript和TypeScript会很有帮助。

let z = 100;

function addToZ(x, y) {
    return x + y + z;
}

函数类型

为函数定义类型

让我们为上面那个函数添加类型:

function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

书写完整函数类型

现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

let myAdd: (x:number, y:number) => number =
    function(x: number, y: number): number { return x + y; };

函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写:

let myAdd: (baseValue: number, increment: number) => number =
    function(x: number, y: number): number { return x + y; };

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。

第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用(=>)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为void而不能留空。

函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。

推断类型

尝试这个例子的时候,你会注意到,就算仅在等式的一侧带有类型,TypeScript编译器仍可正确识别类型:

// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };

// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
    function(x, y) { return x + y; };

这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。

可选参数和默认参数

TypeScript里的每个函数参数都是必须的。 这不是指不能传递nullundefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // ah, just right

JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用?实现可选参数的功能。 比如,我们想让last name是可选的:

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

let result1 = buildName("Bob");  // works correctly now
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");  // ah, just right

可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。

在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined);       // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result4 = buildName("Bob", "Adams");         // ah, just right

在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。

function buildName(firstName: string, lastName?: string) {
    // ...
}

function buildName(firstName: string, lastName = "Smith") {
    // ...
}

共享同样的类型(firstName: string, lastName?: string) => string。 在函数类型中,默认参数的默认值不会显示,而只会显示它是一个可选参数。

与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入undefined值来获得默认值。 例如,我们重写最后一个例子,让firstName是带默认值的参数:

function buildName(firstName = "Will", lastName: string) {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams");     // okay and returns "Will Adams"

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用arguments来访问所有传入的参数。

在TypeScript里,你可以把所有参数收集到一个变量里:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号(...)后面给定的名字,你可以在函数体内使用这个数组。

这个省略号也会在带有剩余参数的函数类型定义上使用到:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

学习如何在JavaScript里正确使用this就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清this工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了this的地方。 如果你想了解JavaScript里的this是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章详细的阐述了this的内部工作原理,因此我们这里只做简单介绍。

this和箭头函数

JavaScript里,this的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。 但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。

下面看一个例子:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        return function() {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

可以看到createCardPicker是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。 因为createCardPicker返回的函数里的this被设置成了window而不是deck对象。 因为我们只是独立地调用了cardPicker()。 顶级的非方法式调用会将this视为window。 (注意:在严格模式下,thisundefined而不是window)。

为了解决这个问题,我们可以在函数被返回时就绑好正确的this。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的this值,而不是调用时的值:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis标记。 它会指出this.suits[pickedSuit]里的this的类型为any

this参数

不幸的是,this.suits[pickedSuit]中的this的类型依旧为any。 这是因为this来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的this参数。 this参数是个假的参数,它出现在参数列表的最前面:

function f(this: void) {
    // make sure `this` is unusable in this standalone function
}

让我们往例子里添加一些接口,CardDeck,让类型重用能够变得清晰简单些:

interface Card {
    suit: string;
    card: number;
}
interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    // NOTE: The function now explicitly specifies that its callee must be of type Deck
    createCardPicker: function(this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说thisDeck类型的,而非any,因此--noImplicitThis不会报错了。

回调函数里的this参数

当你将一个函数传递到某个库函数里在稍后被调用时,你可能也见到过回调函数里的this会报错。 因为当回调函数被调用时,它会被当成一个普通函数调用,this将为undefined。 稍做改动,你就可以通过this参数来避免错误。 首先,库函数的作者要指定this的类型:

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

this: void意味着addClickListener期望onclick是一个函数且它不需要一个this类型。 然后,为调用代码里的this添加类型注解:

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // oops, used this here. using this callback would crash at runtime
        this.info = e.message;
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!

指定了this类型后,你显式声明onClickBad必须在Handler的实例上调用。 然后TypeScript会检测到addClickListener要求函数带有this: void。 改变this类型来修复这个错误:

class Handler {
    info: string;
    onClickGood(this: void, e: Event) {
        // can't use this here because it's of type void!
        console.log('clicked!');
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

因为onClickGood指定了this类型为void,因此传递addClickListener是合法的。 当然了,这也意味着不能使用this.info. 如果你两者都想要,你不得不使用箭头函数了:

class Handler {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message }
}

这是可行的因为箭头函数使用外层的this,所以你总是可以把它们传给期望this: void的函数。 缺点是每个Handler对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到Handler的原型链上。 它们在不同Handler对象间是共享的。

重载

JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

pickCard方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。

方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载pickCard函数。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

这样改变后,重载的pickCard函数在调用的时候会进行正确的类型检查。

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用pickCard会产生错误。

字面量类型

介绍

一个字面量是一个集体类型中更为具体的一种子类型。意思是:"Hello World" 是一个 string,但是一个 string 不是类型系统中的 "Hello World"

目前 TypeScript 中有三种可用的字面量类型集合,分别是:字符串、数字和布尔值。通过使用字面量类型,你可以规定一个字符串、数字或布尔值必须含有的确定值。

字面量收窄

当你通过 varlet 来声明一个变量时,实际上你在告诉编译器这个变量中的内容有可能会被改变。与之相对地,用 const 来声明对象会让 TypeScript 知道这个对象永远不会被改变。

// We're making a guarantee that this variable
// helloWorld will never change, by using const.

// So, TypeScript sets the type to be "Hello World" not string
const helloWorld = "Hello World";

// On the other hand, a let can change, and so the compiler declares it a string
let hiWorld = "Hi World";

从无穷多种可能的例子(string 变量的值有无穷多种)到一个更小、确定数量的例子(在上述例子中,"Hello Wrold" 的可能值只有一种)的过程就叫收窄。

字符串字面量类型

字面量类型可以通过联合联系、类型守卫、类型别名来结合实际字符串值。通过这些特性,我们可以获取一种字符串并使其有类似枚举(enum)的行为。

type Easing = "ease-in" | "ease-out" | "ease-in-out";

class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === "ease-in") {
      // ...
    } else if (easing === "ease-out") {
    } else if (easing === "ease-in-out") {
    } else {
      // It's possible that someone could reach this
      // by ignoring your types though.
    }
  }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy");
// Error: Argument of type '"uneasy"' is not assignable to parameter of type 'Easing'.

你可以传递三种允许的字符串,但是如果传递其他的字符串会收到如下错误:

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字符串字面可以通过相同的方式用来分别重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
  // ... code goes here ...
}

数字字面量类型

TypeScript 还有数字字面量类型,它的行为和上述字符串字面量类型相同。

function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
  return (Math.floor(Math.random() * 6) + 1) as 1 | 2 | 3 | 4 | 5 | 6;
}

const result = rollDice();

数字字面量类型经常用来描述配置值:

interface MapConfig {
  lng: number;
  lat: number;
  tileSize: 8 | 16 | 32;
}

setupMap({ lng: -73.935242, lat: 40.73061, tileSize: 16 });

布尔字面量类型

TypeScript 还有布尔值字面量类型,你可以通过他们来约束某些属性之间互有关联的对象。

interface ValidationSuccess {
  isValid: true;
  reason: null;
};

interface ValidationFailure {
  isValid: false;
  reason: string;
};

type ValidationResult =
  | ValidationSuccess
  | ValidationFailure;

联合类型和交叉类型

介绍

到目前为止,手册已经涵盖了原子对象的类型。 但是,随着对更多类型进行建模,你会发现自己正在寻找可以组合现有类型的工具,而不是从头开始创建它们。

交叉类型和联合类型是组合类型的方式之一。

联合类型

有时,你会遇到一个库,它期望一个参数是 numberstring 。 例如下面的函数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${typeof padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

在上面的例子中,padLeft的问题在于其padding参数的类型为any。 这意味着我们可以用numberstring之外的参数类型来调用它,而TypeScript也能接受。

declare function padLeft(value: string, padding: any): string;
// ---cut---
// 编译时通过但是运行时失败。
let indentedString = padLeft("Hello world", true);

在传统的面向对象编程中,我们会通过创建一个具有层状结构的类型来抽象这两个类型。 虽然这更明确,但也有点矫枉过正。 padLeft的原始版本的一个好处是,我们可以直接传递基本元素。 这意味着用法简单而简洁。 而且如果我们只是想使用一个已经存在于其他地方的函数,这种新方法也无济于事。

为了取代any,我们可以为padding参数使用 联合类型

// @errors: 2345
/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft("Hello world", true);

一个联合类型表示一个值的类型可以是几个类型中的一个。 我们用竖线(|)来分隔不同类型,所以number | string | boolean是一个可以是numberstringboolean的值的类型。

具有公共字段的联合

如果我们有一个联合类型的值,则只能访问联合中所有类型共有的成员。

// @errors: 2339

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

declare function getSmallPet(): Fish | Bird;

let pet = getSmallPet();
pet.layEggs();

// 只有两种可能类型中的一种可用
pet.swim();

联合类型在这里可能有点棘手,但它只是需要一点直觉来适应。 如果一个值的类型是A | B,我们只能 确定 它有A B都有的成员。 在这个例子中,Bird有一个名为fly的成员。 我们不能确定一个类型为Bird | Fish的变量是否有一个fly方法。 如果该变量在运行时确实是Fish,那么调用pet.fly()将会失败。

可区分联合

使用联合的一种常用技术是使用字面量类型的单个字段,您可以使用该字段来缩小 TypeScript 可能的当前类型。例如,我们将创建一个包含三种类型的联合,这些类型具有一个共享字段。

type NetworkLoadingState = {
  state: "loading";
};

type NetworkFailedState = {
  state: "failed";
  code: number;
};

type NetworkSuccessState = {
  state: "success";
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};

// 创建一个只代表上述类型之一的类型,但你还不确定它是哪个。
type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState;

上述类型都以一个名为state的字段,然后它们也有自己的字段。

NetworkLoadingStateNetworkFailedStateNetworkSuccessState
statestatestate
coderesponse

鉴于state字段在NetworkState的每个类型中都是通用的--你的代码无需存在检查即可安全访问。

有了state这个字面类型,你可以将state的值与相应的字符串进行比较,TypeScript就会知道当前使用的是哪个类型。

NetworkLoadingStateNetworkFailedStateNetworkSuccessState
"loading""failed""success"

在这个例子中,你可以使用switch语句来缩小在运行时代表哪种类型:

// @errors: 2339
type NetworkLoadingState = {
  state: "loading";
};

type NetworkFailedState = {
  state: "failed";
  code: number;
};

type NetworkSuccessState = {
  state: "success";
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};
// ---cut---
type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState;

function logger(state: NetworkState): string {
  // 现在,TypeScript不知道state是三种可能类型中的哪一种。

  // 试图访问一个不是所有类型都共享的属性将引发一个错误
  state.code;

  // 通过选择state,TypeScript可以在代码流分析中缩小联合的范围
  switch (state.state) {
    case "loading":
      return "Downloading...";
    case "failed":
      // 这里的类型一定是NetworkFailedState,所以访问`code`字段是安全的。
      return `Error ${state.code} downloading`;
    case "success":
      return `Downloaded ${state.response.title} - ${state.response.summary}`;
  }
}

联合的穷尽性检查

我们希望编译器能在我们没能覆盖可区分联合的所有变体时告诉我们。 比如,如果我们添加NetworkFromCachedStateNetworkState,我们也需要更新logger

// @errors: 2366
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = {
  state: "success";
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};
// ---cut---
type NetworkFromCachedState = {
  state: "from_cache";
  id: string;
  response: NetworkSuccessState["response"];
};

type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState
  | NetworkFromCachedState;

function logger(s: NetworkState) {
  switch (s.state) {
    case "loading":
      return "loading request";
    case "failed":
      return `failed with code ${s.code}`;
    case "success":
      return "got response";
  }
}

这里有两种方法实现。 第一种方法是打开strictNullChecks并指定返回类型:

// @errors: 2366
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = { state: "success" };
type NetworkFromCachedState = { state: "from_cache" };

type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState
  | NetworkFromCachedState;

// ---cut---
function logger(s: NetworkState): string {
  switch (s.state) {
    case "loading":
      return "loading request";
    case "failed":
      return `failed with code ${s.code}`;
    case "success":
      return "got response";
  }
}

因为switch不再是详尽的,TypeScript知道函数有时可能会返回undefined。 如果你有一个明确的返回类型string,那么你会得到一个错误,返回类型实际上是string | undefined。 然而,这种方法是相当微妙的,此外,strictNullChecks并不总是对旧代码起作用。

第二种方法是使用编译器用来检查穷尽性的never类型:

// @errors: 2345
type NetworkLoadingState = { state: "loading" };
type NetworkFailedState = { state: "failed"; code: number };
type NetworkSuccessState = { state: "success" };
type NetworkFromCachedState = { state: "from_cache" };

type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState
  | NetworkFromCachedState;
// ---cut---
function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

function logger(s: NetworkState): string {
  switch (s.state) {
    case "loading":
      return "loading request";
    case "failed":
      return `failed with code ${s.code}`;
    case "success":
      return "got response";
    default:
      return assertNever(s);
  }
}

在这里,assertNever检查s是否属于never类型—即所有其他情况都被移除后剩下的类型。 如果你忘记了这个情况,那么s将会有一个实际的类型,而你将会得到一个类型错误。 这个方法需要你定义一个额外的函数,但是当你忘记的时候就更明显了,因为错误信息中包括了丢失的类型名称。

交叉类型

交叉类型与联合类型密切相关,但它们的使用方式非常不同。 交叉类型将多个类型合并为一个。 这允许你把现有的类型加在一起,得到一个具有你需要的所有功能的单个类型。 例如,Person & Serializable & Loggable是一种类型,它是PersonSerializableLoggable的全部。 这意味着这种类型的对象将拥有这三种类型的所有成员。

例如,如果你有具有一致的错误处理的网络请求,那么你可以将错误处理分离到它自己的类型中,与对应于单个响应类型的类型合并。

interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtworksData {
  artworks: { title: string }[];
}

interface ArtistsData {
  artists: { name: string }[];
}

// 这些接口被组合后拥有一致的错误处理,和它们自己的数据

type ArtworksResponse = ArtworksData & ErrorHandling;
type ArtistsResponse = ArtistsData & ErrorHandling;

const handleArtistsResponse = (response: ArtistsResponse) => {
  if (response.error) {
    console.error(response.error.message);
    return;
  }

  console.log(response.artists);
};

介绍

传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。 使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本。

下面看一个使用类的例子:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

如果你使用过C#或Java,你会对这种语法非常熟悉。 我们声明一个Greeter类。这个类有3个成员:一个叫做greeting的属性,一个构造函数和一个greet方法。

你会注意到,我们在引用任何一个类成员的时候都用了this。 它表示我们访问的是类的成员。

最后一行,我们使用new构造了Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它。

继承

在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

看下面的例子:

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog是一个_派生类_,它派生自Animal基类,通过extends关键字。 派生类通常被称作_子类_,基类通常被称作_超类_。

因为Dog继承了Animal的功能,因此我们可以创建一个Dog的实例,它能够bark()move()

下面我们来看个更加复杂的例子。

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

这个例子展示了一些上面没有提到的特性。 这一次,我们使用extends关键字创建了Animal的两个子类:HorseSnake

与前一个例子的不同点是,派生类包含了一个构造函数,它_必须_调用super(),它会执行基类的构造函数。 而且,在构造函数里访问this的属性之前,我们_一定_要调用super()。 这个是TypeScript强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。 Snake类和Horse类都创建了move方法,它们重写了从Animal继承来的move方法,使得move方法根据不同的类而具有不同的功能。 注意,即使tom被声明为Animal类型,但因为它的值是Horse,调用tom.move(34)时,它会调用Horse里重写的方法:

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

公共,私有与受保护的修饰符

默认为public

在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用public来做修饰;例如,C#要求必须明确地使用public指定成员是可见的。 在TypeScript里,成员都默认为public

你也可以明确的将一个成员标记成public。 我们可以用下面的方式来重写上面的Animal类:

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

理解private

当成员被标记成private时,它就不能在声明它的类的外部访问。比如:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.

TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

然而,当我们比较带有privateprotected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于protected成员也使用这个规则。

下面来看一个例子,更好地说明了这一点:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // 错误: Animal 与 Employee 不兼容.

这个例子中有AnimalRhino两个类,RhinoAnimal类的子类。 还有一个Employee类,其类型看上去与Animal是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。 因为AnimalRhino共享了来自Animal里的私有成员定义private name: string,因此它们是兼容的。 然而Employee却不是这样。当把Employee赋值给Animal的时候,得到一个错误,说它们的类型不兼容。 尽管Employee里也有一个私有成员name,但它明显不是Animal里面定义的那个。

理解protected

protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。例如:

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

注意,我们不能在Person类外使用name,但是我们仍然可以通过Employee类的实例方法访问,因为Employee是由Person派生而来的。

构造函数也可以被标记成protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee 能够继承 Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

readonly修饰符

你可以使用readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

参数属性

在上面的例子中,我们不得不在在Person类里定义一个只读成员name和一个构造函数参数theName。这样做是为了在Octopus构造函数被执行后,就可以访问theName的值。 这种情况经常会遇到。_参数属性_可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前Animal类的修改版,使用了参数属性:

class Animal {
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

注意看我们是如何舍弃了theName,仅在构造函数里使用private name: string参数来创建和初始化name成员。 我们把声明和赋值合并至一处。

参数属性通过给构造函数参数添加一个访问限定符来声明。 使用private限定一个参数属性会声明并初始化一个私有成员;对于publicprotected来说也是一样。

存取器

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用getset。 首先,我们从一个没有使用存取器的例子开始。

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

允许随意设置fullName虽然方便,但是我们仍想在设置fullName强制执行某些约束。

在这个版本里,我们添加一个setter来检查newName的长度,以确保它满足数据库字段的最大长度限制。若它不满足,那么我们就抛一个错误来告诉客户端出错了。

为保留原有的功能,我们同时添加一个getter用来读取fullName

const fullNameMaxLength = 10;

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (newName && newName.length > fullNameMaxLength) {
            throw new Error("fullName has a max length of " + fullNameMaxLength);
        }

        this._fullName = newName;
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

为证明我们写的存取器现在能检查长度,我们可以给名字赋一个长度大于10字符的值,并验证是否得到一个错误。

对于存取器有下面几点需要注意的:

首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有get不带有set的存取器自动被推断为readonly。 这在从代码生成.d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。

静态属性

到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用static定义origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在origin前面加上类名。 如同在实例属性上使用this.前缀来访问属性一样,这里我们使用Grid.来访问静态属性。

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节(抽象类中除抽象函数之外,其他函数可以包含具体实现)。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含abstract关键字并且可以包含访问修饰符。

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

高级技巧

构造函数

当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的_实例_的类型。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

这里,我们写了let greeter: Greeter,意思是Greeter类的实例的类型是Greeter。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。

我们也创建了一个叫做_构造函数_的值。 这个函数会在我们使用new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

上面的代码里,let Greeter将被赋值为构造函数。 当我们调用new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有_实例部分_与_静态部分_这两个部分。

让我们稍微改写一下这个例子,看看它们之间的区别:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

这个例子里,greeter1与之前看到的一样。 我们实例化Greeter类,并使用这个对象。 与我们之前看到的一样。

再之后,我们直接使用类。 我们创建了一个叫做greeterMaker的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,"告诉我Greeter标识符的类型",也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在greeterMaker上使用new,创建Greeter的实例。

把类当做接口使用

如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

枚举

枚举

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。

数字枚举

首先我们看看数字枚举,如果你使用过其它编程语言应该会很熟悉。

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

如上,我们定义了一个数字枚举,Up使用初始化为1。 其余的成员会从1开始自动增长。 换句话说,Direction.Up的值为1Down2Left3Right4

我们还可以完全不使用初始化器:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

现在,Up的值为0Down的值为1等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。

使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

数字枚举可以被混入到计算过的和常量成员(如下所示)。 简短地说,没有初始化器的成员要么在首位,要么必须在用数值常量或其他常量枚举成员初始化的数值枚举之后。 换句话说,下面的情况是不被允许的:

enum E {
    A = getSomeValue(),
    B, // Error! Enum member must have initializer.
}

字符串枚举

字符串枚举的概念很简单,但是有细微的运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。 换句话说,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的 - 它并不能表达有用的信息(尽管反向映射会有所帮助),字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。

异构枚举(Heterogeneous enums)

从技术的角度来说,枚举可以混合字符串和数字成员,但是似乎你并不会这么做:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

除非你真的想要利用JavaScript运行时的行为,否则我们不建议这样做。

计算的和常量成员

每个枚举成员都带有一个值,它可以是_常量_或_计算出来的_。 当满足如下条件时,枚举成员被当作是常量:

  • 它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值0

    // E.X is constant:
    enum E { X }
    
  • 它不带有初始化器且它之前的枚举成员是一个_数字_常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1。

    // All enum members in 'E1' and 'E2' are constant.
    
    enum E1 { X, Y, Z }
    
    enum E2 {
        A = 1, B, C
    }
    
  • 枚举成员使用_常量枚举表达式_初始化。 常量枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:

    1. 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
    2. 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
    3. 带括号的常量枚举表达式
    4. 一元运算符+, -, ~其中之一应用在了常量枚举表达式
    5. 常量枚举表达式做为二元运算符+, -, *, /, %, <<, >>, >>>, &, |, ^的操作对象。

    若常量枚举表达式求值后为NaNInfinity,则会在编译阶段报错。

所有其它情况的枚举成员被当作是需要计算得出的值。

enum FileAccess {
    // constant members
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // computed member
    G = "123".length
}

联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。 字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为

  • 任何字符串字面量(例如:"foo""bar""baz"
  • 任何数字字面量(例如:1, 100
  • 应用了一元-符号的数字字面量(例如:-1, -100

当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。

首先,枚举成员成为了类型! 例如,我们可以说某些成员_只能_是枚举成员的值:

enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

let c: Circle = {
    kind: ShapeKind.Square, // Error! Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
    radius: 100,
}

另一个变化是枚举类型本身变成了每个枚举成员的_联合_。 虽然我们还没有讨论联合类型,但你只要知道通过联合枚举,类型系统能够利用这样一个事实,它可以知道枚举里的值的集合。 因此,TypeScript能够捕获在比较值的时候犯的愚蠢的错误。 例如:

enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
        //             ~~~~~~~~~~~
        // Error! This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
    }
}

这个例子里,我们先检查x是否不是E.Foo。 如果通过了这个检查,然后||会发生短路效果,if语句体里的内容会被执行。 然而,这个检查没有通过,那么x则_只能_为E.Foo,因此没理由再去检查它是否为E.Bar

运行时的枚举

枚举是在运行时真正存在的对象。 例如下面的枚举:

enum E {
    X, Y, Z
}

可以传递给函数

function f(obj: { X: number }) {
    return obj.X;
}

// 没问题,因为 'E'包含一个数值型属性'X'。
f(E);

编译时的枚举

尽管一个枚举是在运行时真正存在的对象,但keyof关键字的行为与其作用在对象上时有所不同。应该使用keyof typeof来获取一个表示枚举里所有字符串key的类型。

enum LogLevel {
    ERROR, WARN, INFO, DEBUG
}

/**
 * 等同于:
 * type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
 */
type LogLevelStrings = keyof typeof LogLevel;

function printImportant(key: LogLevelStrings, message: string) {
    const num = LogLevel[key];
    if (num <= LogLevel.WARN) {
       console.log('Log level key is: ', key);
       console.log('Log level value is: ', num);
       console.log('Log level message is: ', message);
    }
}
printImportant('ERROR', 'This is a message');

反向映射

除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了_反向映射_,从枚举值到枚举名字。 例如,在下面的例子中:

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

TypeScript可能会将这段代码编译为下面的JavaScript:

var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

生成的代码中,枚举类型被编译成一个对象,它包含了正向映射(name -> value)和反向映射(value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。

要注意的是_不会_为字符串枚举成员生成反向映射。

const枚举

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用const枚举。 常量枚举通过在枚举上使用const修饰符来定义。

const enum Enum {
    A = 1,
    B = A * 2
}

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

生成后的代码为:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

外部枚举

外部枚举用来描述已经存在的枚举类型的形状。

declare enum Enum {
    A = 1,
    B,
    C = 2
}

外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常量成员。 对于非常量的外部枚举而言,没有初始化方法时被当做需要经过计算的。

泛型

介绍

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

泛型之Hello World

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是echo命令。

不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

或者,我们使用any类型来定义函数:

function identity(arg: any): any {
    return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。 如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了_类型变量_,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
    return arg;
}

我们给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString");  // type of output will be 'string'

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍。利用了_类型推论_ -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");  // type of output will be 'string'

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

function identity<T>(arg: T): T {
    return arg;
}

如果我们想同时打印出arg的长度。 我们很可能会这样做:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

如果这么做,编译器会报错说我们使用了arg.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有.length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一节,会介绍如何创建自定义泛型像Array<T>一样。

泛型类型

上一节,我们创建了identity通用函数,可以适用于不同的类型。 在这节,我们研究一下函数本身的类型,以及如何创建泛型接口。

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如:Dictionary<string>而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

注意,我们的示例做了少许改动。 不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。 当我们使用GenericIdentityFn的时候,还得传入一个类型参数来指定泛型类型(这里是:number),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。

除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用(<>)括起泛型类型,跟在类名后面。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

我们在那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型约束

你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含.length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象obj上,因此我们需要在这两个类型之间使用约束。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,

function create<T>(c: {new(): T; }): T {
    return new c();
}

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

手册(进阶)

高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable同时是PersonSerializableLoggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子("target": "es5"):

function extend<First, Second>(first: First, second: Second): First & Second {
    const result: Partial<First & Second> = {};
    for (const prop in first) {
        if (first.hasOwnProperty(prop)) {
            (result as First)[prop] = first[prop];
        }
    }
    for (const prop in second) {
        if (second.hasOwnProperty(prop)) {
            (result as Second)[prop] = second[prop];
        }
    }
    return result as First & Second;
}

class Person {
    constructor(public name: string) { }
}

interface Loggable {
    log(name: string): void;
}

class ConsoleLogger implements Loggable {
    log(name) {
        console.log(`Hello, I'm ${name}.`);
    }
}

const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);

联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入numberstring类型的参数。 例如下面的函数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

padLeft存在一个问题,padding参数的类型指定成了any。 这就是说我们可以传入一个既不是number也不是string类型的参数,但是TypeScript却不报错。

let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错

在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。

代替any, 我们可以使用_联合类型_做为padding的参数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是numberstring,或boolean

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是A | B,我们能够_确定_的是它包含了AB中共有的成员。 这个例子里,Bird具有一个fly成员。 我们不能确定一个Bird | Fish类型的变量是否有fly方法。 如果变量在运行时是Fish类型,那么调用pet.fly()就出错了。

类型守卫与类型区分(Type Guards and Differentiating Types)

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。

let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

为了让这段代码工作,我们要使用类型断言:

let pet = getSmallPet();

if ((pet as Fish).swim) {
    (pet as Fish).swim();
} else if ((pet as Bird).fly) {
    (pet as Bird).fly();
}

用户自定义的类型守卫

这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet的类型的话就好了。

TypeScript里的_类型守卫_机制让它成为了现实。 类型守卫就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

使用类型判定

要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个_类型谓词_:

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}

在这个例子里,pet is Fish就是类型谓词。 谓词为parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

注意TypeScript不仅知道在if分支里petFish类型; 它还清楚在else分支里,一定_不是_Fish类型,一定是Bird类型。

使用in操作符

in操作符可以作为类型细化表达式来使用。

对于n in x表达式,其中n是字符串字面量或字符串字面量类型且x是个联合类型,那么true分支的类型细化为有一个可选的或必须的属性nfalse分支的类型细化为有一个可选的或不存在属性n

function move(pet: Fish | Bird) {
    if ("swim" in pet) {
        return pet.swim();
    }
    return pet.fly();
}

typeof类型守卫

现在我们回过头来看看怎么使用联合类型书写padLeft代码。 我们可以像下面这样利用类型断言来写:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型守卫。 也就是说我们可以直接在代码里检查类型了。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

这些_typeof类型守卫_只有两种形式能被识别:typeof v === "typename"typeof v !== "typename""typename"必须是"number""string""boolean""symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型守卫。

instanceof类型守卫

如果你已经阅读了typeof类型守卫并且对JavaScript里的instanceof操作符熟悉的话,你可能已经猜到了这节要讲的内容。

_instanceof类型守卫_是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  1. 此构造函数的prototype属性的类型,如果它的类型不为any的话
  2. 构造签名所返回的类型的联合

以此顺序。

可以为null的类型

TypeScript具有两种特殊的类型,nullundefined,它们分别具有值nullundefined. 我们在基础类型一节里已经做过简要说明。 默认情况下,类型检查器认为nullundefined可以赋值给任何类型。 nullundefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为价值亿万美金的错误

--strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含nullundefined。 你可以使用联合类型明确的包含它们:

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

注意,按照JavaScript的语义,TypeScript会把nullundefined区别对待。 string | nullstring | undefinedstring | undefined | null是不同的类型。

可选参数和可选属性

使用了--strictNullChecks,可选参数会被自动地加上| undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型守卫和类型断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型守卫来去除null。 幸运地是这与在JavaScript里写的代码一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

这里很明显地去除了null,你也可以使用短路运算符:

function f(sn: string | null): string {
    return sn || "default";
}

如果编译器不能够去除nullundefined,你可以使用类型断言手动去除。 语法是添加!后缀:identifier!identifier的类型里去除了nullundefined

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时name的类型。

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

起别名不会新建一个类型 - 它创建了一个新_名字_来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

type Container<T> = { value: T };

我们也可以使用类型别名来在属性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

然而,类型别名不能出现在声明右侧的任何地方。

type Yikes = Array<Yikes>; // error

接口 vs. 类型别名

像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。

其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在interfaced上,显示它返回的是Interface,但悬停在aliased上时,显示的却是对象字面量类型。

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

在旧版本的TypeScript里,类型别名不能被继承和实现(它们也不能继承和实现其它类型)。从TypeScript 2.7开始,类型别名可以被继承并生成新的交叉类型。例如:type Cat = Animal & { purrs: true }

因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。

另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型守卫和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

数字字面量类型

TypeScript还具有数字字面量类型。

function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

换句话说,当x2进行比较的时候,它的值必须为1,这就意味着上面的比较检查是非法的。

枚举成员类型

如我们在枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。

可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型守卫和类型别名来创建一个叫做_可辨识联合_的高级模式,它也称做_标签联合_或_代数数据类型_。 可辨识联合在函数式编程里很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

  1. 具有普通的单例类型属性—可辨识的特征
  2. 一个类型别名包含了那些类型的联合—联合
  3. 此属性上的类型守卫。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

首先我们声明了将要联合的接口。 每个接口都有kind属性但有不同的字符串字面量类型。 kind属性称做_可辨识的特征_或_标签_。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:

type Shape = Square | Rectangle | Circle;

现在我们使用可辨识联合:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了TriangleShape,我们同时还需要更新area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

有两种方式可以实现。 首先是启用--strictNullChecks并且指定一个返回值类型:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

因为switch没有包含所有情况,所以TypeScript认为这个函数有时候会返回undefined。 如果你明确地指定了返回值类型为number,那么你会看到一个错误,因为实际上返回值的类型为number | undefined。 然而,这种方法存在些微妙之处且--strictNullChecks对旧代码支持不好。

第二种方法使用never类型,编译器用它来进行完整性检查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

这里,assertNever检查s是否为never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

多态的this类型

多态的this类型表示的是某个包含类或接口的_子类型_。 这被称做_F_-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回this类型:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

由于这个类使用了this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

如果没有this类型,ScientificCalculator就不能够在继承BasicCalculator的同时还保持接口的连贯性。 multiply将会返回BasicCalculator,它并没有sin方法。 然而,使用this类型,multiply会返回this,在这里就是ScientificCalculator

索引类型(Index types)

使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。

function pluck(o, propertyNames) {
    return propertyNames.map(n => o[n]);
}

下面是如何在TypeScript里使用此函数,通过索引类型查询索引访问操作符:

function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
  return propertyNames.map(n => o[n]);
}

interface Car {
    manufacturer: string;
    model: string;
    year: number;
}
let taxi: Car = {
    manufacturer: 'Toyota',
    model: 'Camry',
    year: 2014
};

// Manufacturer and model are both of type string,
// so we can pluck them both into a typed string array
let makeAndModel: string[] = pluck(taxi, ['manufacturer', 'model']);

// If we try to pluck model and year, we get an
// array of a union type: (string | number)[]
let modelYear = pluck(taxi, ['model', 'year'])

编译器会检查manufacturermodel是否真的是Car上的一个属性。 本例还引入了几个新的类型操作符。 首先是keyof T索引类型查询操作符。 对于任何类型Tkeyof T的结果为T上已知的公共属性名的联合。 例如:

let carProps: keyof Car; // the union of ('manufacturer' | 'model' | 'year')

keyof Car是完全可以与'manufacturer' | 'model' | 'year'互相替换的。 不同的是如果你添加了其它的属性到Car,例如ownersAddress: string,那么keyof Car会自动变为'manufacturer' | 'model' | 'year' | 'ownersAddress'。 你可以在像pluck函数这类上下文里使用keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给pluck

// error, 'unknown' is not in 'manufacturer' | 'model' | 'year'
pluck(taxi, ['year', 'unknown']);

第二个操作符是T[K]索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着person['name']具有类型Person['name'] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用T[K],这正是它的强大所在。 你只要确保类型变量K extends keyof T就可以了。 例如下面getProperty函数的例子:

function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
    return o[propertyName]; // o[propertyName] is of type T[K]
}

getProperty里的o: TpropertyName: K,意味着o[propertyName]: T[K]。 当你返回T[K]的结果,编译器会实例化键的真实类型,因此getProperty的返回值类型会随着你需要的属性改变。

let name: string = getProperty(taxi, 'manufacturer');
let year: number = getProperty(taxi, 'year');

// error, 'unknown' is not in 'manufacturer' | 'model' | 'year'
let unknown = getProperty(taxi, 'unknown');

索引类型和字符串索引签名

keyofT[K]与字符串索引签名进行交互。索引签名的参数类型必须为numberstring。 如果你有一个带有字符串索引签名的类型,那么keyof T会是string | number。 (并非只有string,因为在JavaScript里,你可以使用字符串object['42']或 数字object[42]索引来访问对象属性)。 并且T[string]为索引签名的类型:

interface Dictionary<T> {
    [key: string]: T;
}
let keys: keyof Dictionary<number>; // string | number
let value: Dictionary<number>['foo']; // number

如果一个类型带有数字索引签名,那么keyof Tnumber

interface Dictionary<T> {
    [key: number]: T;
}
let keys: keyof Dictionary<number>; // number
let value: Dictionary<number>['foo']; // Error, Property 'foo' does not exist on type 'Dictionary<number>'.
let value: Dictionary<number>[42]; // number

映射类型

一个常见的任务是将一个已知的类型每个属性都变为可选的:

interface PersonPartial {
    name?: string;
    age?: number;
}

或者我们想要一个只读版本:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为readonly类型或可选的。 下面是一些例子:

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

像下面这样使用:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

需要注意的是这个语法描述的是类型而非成员。 若想添加成员,则可以使用交叉类型:

// 这样使用
type PartialWithNewMember<T> = {
  [P in keyof T]?: T[P];
} & { newMember: boolean }
// 不要这样使用
// 这会报错!
type PartialWithNewMember<T> = {
  [P in keyof T]?: T[P];
  newMember: boolean;
}

下面来看看最简单的映射类型和它的组成部分:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的语法与索引签名的语法类型,内部使用了for .. in。 具有三个部分:

  1. 类型变量K,它会依次绑定到每个属性。
  2. 字符串字面量联合的Keys,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

在个简单的例子里,Keys是硬编码的属性名列表并且属性类型永远是boolean,因此这个映射类型等同于:

type Flags = {
    option1: boolean;
    option2: boolean;
}

在真正的应用里,可能不同于上面的ReadonlyPartial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是keyof和索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

在这些例子里,属性列表是keyof T且结果类型是T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是同态的,映射只作用于T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设Person.name是只读的,那么Partial<Person>.name也将是只读的且为可选的。

下面是另一个例子,T[P]被包装在Proxy<T>类里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

注意Readonly<T>Partial<T>用处不小,因此它们与PickRecord一同被包含进了TypeScript的标准库里:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends keyof any, T> = {
    [P in K]: T;
}

ReadonlyPartialPick是同态的,但Record不是。 因为Record并不需要输入类型来拷贝属性,所以它不属于同态:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

由映射类型进行推断

现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

有条件类型

TypeScript 2.8引入了_有条件类型_,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y

上面的类型意思是,若T能够赋值给U,那么类型是X,否则为Y

有条件的类型T extends U ? X : Y或者_解析_为X,或者_解析_为Y,再或者_延迟_解析,因为它可能依赖一个或多个类型变量。 若TU包含类型参数,那么是否解析为XY或推迟,取决于类型系统是否有足够的信息来确定T总是可以赋值给U

下面是一些类型可以被立即解析的例子:

declare function f<T extends boolean>(x: T): T extends true ? string : number;

// Type is 'string | number
let x = f(Math.random() < 0.5)

另外一个例子涉及TypeName类型别名,它使用了嵌套了有条件类型:

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

下面是一个有条件类型被推迟解析的例子:

interface Foo {
    propA: boolean;
    propB: boolean;
}

declare function f<T>(x: T): T extends Foo ? string : number;

function foo<U>(x: U) {
    // Has type 'U extends Foo ? string : number'
    let a = f(x);

    // This assignment is allowed though!
    let b: string | number = a;
}

这里,a变量含有未确定的有条件类型。 当有另一段代码调用foo,它会用其它类型替换U,TypeScript将重新计算有条件类型,决定它是否可以选择一个分支。

与此同时,我们可以将有条件类型赋值给其它类型,只要有条件类型的每个分支都可以赋值给目标类型。 因此在我们的例子里,我们可以将U extends Foo ? string : number赋值给string | number,因为不管这个有条件类型最终结果是什么,它只能是stringnumber

分布式有条件类型

如果有条件类型里待检查的类型是naked type parameter,那么它也被称为“分布式有条件类型”。 分布式有条件类型在实例化时会自动分发成联合类型。 例如,实例化T extends U ? X : YT的类型为A | B | C,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

例子

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T12 = TypeName<string | string[] | undefined>;  // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>;  // "object"

T extends U ? X : Y的实例化里,对T的引用被解析为联合类型的一部分(比如,T指向某一单个部分,在有条件类型分布到联合类型之后)。 此外,在X内对T的引用有一个附加的类型参数约束U(例如,T被当成在X内可赋值给U)。

例子

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[]>;  // BoxedArray<number>;
type T22 = Boxed<string | number[]>;  // BoxedValue<string> | BoxedArray<number>;

注意在Boxed<T>true分支里,T有个额外的约束any[],因此它适用于T[number]数组元素类型。同时也注意一下有条件类型是如何分布成联合类型的。

有条件类型的分布式的属性可以方便地用来_过滤_联合类型:

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>;  // string | number
type T33 = Filter<string | number | (() => void), Function>;  // () => void

type NonNullable<T> = Diff<T, null | undefined>;  // Remove null and undefined from T

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

function f1<T>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}

有条件类型与映射类型结合时特别有用:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

与联合类型和交叉类型相似,有条件类型不允许递归地引用自己。比如下面的错误。

例子

// 在 TypeScript 4.1 之前的版本会报错。
// TypeScript 4.1 改进了对递归的有条件类型的支持,详情参考 4.1 版本发布说明
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;

有条件类型中的类型推断

现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer

例如,下面代码会提取函数类型的返回值类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

下面的例子解释了在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

相似地,在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

当推断具有多个调用签名(例如函数重载类型)的类型时,用_最后_的签名(大概是最自由的包含所有情况的签名)进行推断。 无法根据参数类型列表来解析重载。

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>;  // string | number

无法在正常类型参数的约束子语句中使用infer声明:

type ReturnType<T extends (...args: any[]) => infer R> = R;  // 错误,不支持

但是,可以这样达到同样的效果,在约束里删掉类型变量,用有条件类型替换:

type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;

预定义的有条件类型

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

Example

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // never
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // never
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

注意:Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。

实用工具类型

TypeScript 提供一些工具类型来帮助常见的类型转换。这些类型是全局可见的。

目录

Partial<Type>

构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。

例子

interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
};

const todo2 = updateTodo(todo1, {
    description: 'throw out trash',
});

Readonly<Type>

构造类型Type,并将它所有的属性设置为readonly,也就是说构造出的类型的属性不能被再次赋值。

例子

interface Todo {
    title: string;
}

const todo: Readonly<Todo> = {
    title: 'Delete inactive users',
};

todo.title = 'Hello'; // Error: cannot reassign a readonly property

这个工具可用来表示在运行时会失败的赋值表达式(比如,当尝试给冻结对象的属性再次赋值时)。

Object.freeze

function freeze<T>(obj: T): Readonly<T>;

Record<Keys, Type>

构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具可用来将某个类型的属性映射到另一个类型上。

例子

interface PageInfo {
    title: string;
}

type Page = 'home' | 'about' | 'contact';

const x: Record<Page, PageInfo> = {
    about: { title: 'about' },
    contact: { title: 'contact' },
    home: { title: 'home' },
};

Pick<Type, Keys>

从类型Type中挑选部分属性Keys来构造类型。

例子

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
};

Omit<Type, Keys>

从类型Type中获取所有属性,然后从中剔除Keys属性后构造一个类型。

例子

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Omit<Todo, 'description'>;

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
};

Exclude<Type, ExcludedUnion>

从类型Type中剔除所有可以赋值给ExcludedUnion的属性,然后构造一个类型。

例子

type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract<Type, Union>

从类型Type中提取所有可以赋值给Union的类型,然后构造一个类型。

例子

type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () => void

NonNullable<Type>

从类型Type中剔除nullundefined,然后构造一个类型。

例子

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

Parameters<Type>

由函数类型Type的参数类型来构建出一个元组类型。

例子

declare function f1(arg: { a: number; b: string }): void;

type T0 = Parameters<() => string>;
//    []
type T1 = Parameters<(s: string) => void>;
//    [s: string]
type T2 = Parameters<<T>(arg: T) => T>;
//    [arg: unknown]
type T3 = Parameters<typeof f1>;
//    [arg: { a: number; b: string; }]
type T4 = Parameters<any>;
//    unknown[]
type T5 = Parameters<never>;
//    never
type T6 = Parameters<string>;
//   never
//   Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T7 = Parameters<Function>;
//   never
//   Type 'Function' does not satisfy the constraint '(...args: any) => any'.

ConstructorParameters<Type>

由构造函数类型来构建出一个元组类型或数组类型。 由构造函数类型Type的参数类型来构建出一个元组类型。(若Type不是构造函数类型,则返回never)。

例子

type T0 = ConstructorParameters<ErrorConstructor>;
//    [message?: string | undefined]
type T1 = ConstructorParameters<FunctionConstructor>;
//    string[]
type T2 = ConstructorParameters<RegExpConstructor>;
//    [pattern: string | RegExp, flags?: string | undefined]
type T3 = ConstructorParameters<any>;
//   unknown[]

type T4 = ConstructorParameters<Function>;
//    never
// Type 'Function' does not satisfy the constraint 'new (...args: any) => any'.

ReturnType<Type>

由函数类型Type的返回值类型构建一个新类型。

例子

type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s: string) => void>;  // void
type T2 = ReturnType<(<T>() => T)>;  // {}
type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T4 = ReturnType<typeof f1>;  // { a: number, b: string }
type T5 = ReturnType<any>;  // any
type T6 = ReturnType<never>;  // any
type T7 = ReturnType<string>;  // Error
type T8 = ReturnType<Function>;  // Error

InstanceType<Type>

由构造函数类型Type的实例类型来构建一个新类型。

例子

class C {
    x = 0;
    y = 0;
}

type T0 = InstanceType<typeof C>; // C
type T1 = InstanceType<any>; // any
type T2 = InstanceType<never>; // any
type T3 = InstanceType<string>; // Error
type T4 = InstanceType<Function>; // Error

Required<Type>

构建一个类型,使类型Type的所有属性为required。 与此相反的是Partial

例子

interface Props {
    a?: number;
    b?: string;
}

const obj: Props = { a: 5 }; // OK

const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing

ThisParameterType<Type>

从函数类型中提取 this 参数的类型。 若函数类型不包含 this 参数,则返回 unknown 类型。

例子

function toHex(this: Number) {
    return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
    return toHex.apply(n);
}

OmitThisParameter<Type>

Type类型中剔除 this 参数。 若未声明 this 参数,则结果类型为 Type 。 否则,由Type类型来构建一个不带this参数的类型。 泛型会被忽略,并且只有最后的重载签名会被采用。

例子

function toHex(this: Number) {
    return this.toString(16);
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

console.log(fiveToHex());

ThisType<Type>

这个工具不会返回一个转换后的类型。 它做为上下文的this类型的一个标记。 注意,若想使用此类型,必须启用--noImplicitThis

例子

// Compile with --noImplicitThis

type ObjectDescriptor<D, M> = {
    data?: D;
    methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return { ...data, ...methods } as D & M;
}

let obj = makeObject({
    data: { x: 0, y: 0 },
    methods: {
        moveBy(dx: number, dy: number) {
            this.x += dx; // Strongly typed this
            this.y += dy; // Strongly typed this
        },
    },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

上面例子中,makeObject参数里的methods对象具有一个上下文类型ThisType<D & M>,因此methods对象的方法里this的类型为{ x: number, y: number } & { moveBy(dx: number, dy: number): number }

lib.d.ts里,ThisType<T>标识接口是个简单的空接口声明。除了在被识别为对象字面量的上下文类型之外,这个接口与一般的空接口没有什么不同。

操作字符串的类型

为了便于操作模版字符串字面量,TypeScript 引入了一些能够操作字符串的类型。 更多详情,请阅读模版字面量类型

Decorators

介绍

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

注意  装饰器是一项实验性特性,在未来的版本中可能会发生改变。

若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

命令行:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

装饰器

_装饰器_是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

例如,有一个@sealed装饰器,我们会这样定义sealed函数:

function sealed(target) {
    // do something with "target" ...
}

注意  后面类装饰器小节里有一个更加详细的例子。

装饰器工厂

如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 _装饰器工厂_就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

我们可以通过下面的方式来写一个装饰器工厂函数:

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

注意  下面方法装饰器小节里有一个更加详细的例子。

装饰器组合

多个装饰器可以同时应用到一个声明上,就像下面的示例:

  • 书写在同一行上:
@f @g x
  • 书写在多行上:
@f
@g
x

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合_f_和_g_时,复合的结果(fg)(x)等同于_f_(g(x))。

同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用。

如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

在控制台里会打印出如下结果:

f(): evaluated
g(): evaluated
g(): called
f(): called

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是_方法装饰器_,访问符装饰器,或_属性装饰器_应用到每个实例成员。
  2. 参数装饰器,然后依次是_方法装饰器_,访问符装饰器,或_属性装饰器_应用到每个静态成员。
  3. _参数装饰器_应用到构造函数。
  4. _类装饰器_应用到类。

类装饰器

_类装饰器_在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中(.d.ts),也不能用在任何外部上下文中(比如declare的类)。

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

注意  如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中_不会_为你做这些。

下面是使用类装饰器(@sealed)的例子,应用在Greeter类:

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以这样定义@sealed装饰器:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)

下面是一个重载构造函数的例子。

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

方法装饰器

_方法装饰器_声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的_属性描述符_上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的_属性描述符_。

注意  如果代码输出目标版本小于ES5,_属性描述符_将会是undefined

如果方法装饰器返回一个值,它会被用作方法的_属性描述符_。

注意  如果代码输出目标版本小于ES5返回值会被忽略。

下面是一个方法装饰器(@enumerable)的例子,应用于Greeter类的方法上:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

我们可以用下面的函数声明来定义@enumerable装饰器:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

这里的@enumerable(false)是一个装饰器工厂。 当装饰器@enumerable(false)被调用时,它会修改属性描述符的enumerable属性。

访问器装饰器

_访问器装饰器_声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的_属性描述符_并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里。

注意  TypeScript不允许同时装饰一个成员的getset访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个_属性描述符_时,它联合了getset访问器,而不是分开声明的。

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的_属性描述符_。

注意  如果代码输出目标版本小于ES5,_Property Descriptor_将会是undefined

如果访问器装饰器返回一个值,它会被用作方法的_属性描述符_。

注意  如果代码输出目标版本小于ES5返回值会被忽略。

下面是使用了访问器装饰器(@configurable)的例子,应用于Point类的成员上:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

我们可以通过如下函数声明来定义@configurable装饰器:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

属性装饰器

_属性装饰器_声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。

注意  _属性描述符_不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。 因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

如果访问符装饰器返回一个值,它会被用作方法的_属性描述符_。

我们可以用它来记录这个属性的元数据,如下例所示:

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

然后定义@format装饰器和getFormat函数:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

这个@format("Hello, %s")装饰器是个 装饰器工厂。 当@format("Hello, %s")被调用时,它添加一条这个属性的元数据,通过reflect-metadata库里的Reflect.metadata函数。 当getFormat被调用时,它读取格式的元数据。

注意  这个例子需要使用reflect-metadata库。 查看元数据了解reflect-metadata库更详细的信息。

参数装饰器

_参数装饰器_声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如declare的类)里。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

注意  参数装饰器只能用来监视一个方法的参数是否被传入。

参数装饰器的返回值会被忽略。

下例定义了参数装饰器(@required)并应用于Greeter类方法的一个参数:

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

然后我们使用下面的函数定义 @required@validate 装饰器:

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

@required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。

注意  这个例子使用了reflect-metadata库。 查看元数据了解reflect-metadata库的更多信息。

元数据

一些例子使用了reflect-metadata库来支持实验性的metadata API。 这个库还不是ECMAScript (JavaScript)标准的一部分。 然而,当装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。

你可以通过npm安装这个库:

npm i reflect-metadata --save

TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或tsconfig.json里启用emitDecoratorMetadata编译器选项。

Command Line:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

当启用后,只要reflect-metadata库被引入了,设计阶段添加的类型信息可以在运行时使用。

如下例所示:

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
        set.call(target, value);
    }
}

TypeScript编译器可以通过@Reflect.metadata装饰器注入设计阶段的类型信息。 你可以认为它相当于下面的TypeScript:

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

注意  装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。

声明合并

介绍

TypeScript中有些独特的概念可以在类型层面上描述JavaScript对象的模型。 这其中尤其独特的一个例子是“声明合并”的概念。 理解了这个概念,将有助于操作现有的JavaScript代码。 同时,也会有助于理解更多高级抽象的概念。

对本文件来讲,“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。

基础概念

TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。 创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。 创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。 最后,创建值的声明会创建在JavaScript输出中看到的值。

Declaration TypeNamespaceTypeValue
NamespaceXX
ClassXX
EnumXX
InterfaceX
Type AliasX
FunctionX
VariableX

理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。

合并接口

最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

接口的非函数的成员应该是唯一的。 如果它们不是唯一的,那么它们必须是相同的类型。 如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口A与后来的接口A合并时,后面的接口具有更高的优先级。

如下例所示:

interface Cloner {
    clone(animal: Animal): Animal;
}

interface Cloner {
    clone(animal: Sheep): Sheep;
}

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}

这三个接口合并成一个声明:

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}

注意每组接口里的声明顺序保持不变,但各组接口之间的顺序是后来的接口重载出现在靠前位置。

这个规则有一个例外是当出现特殊的函数签名时。 如果签名里有一个参数的类型是_单一_的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端。

比如,下面的接口会合并到一起:

interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
    createElement(tagName: string): HTMLElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}

合并后的Document将会像下面这样:

interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}

合并命名空间

与接口相似,同名的命名空间也会合并其成员。 命名空间会创建出命名空间和值,我们需要知道这两者都是怎么合并的。

对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。

对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。

Animals声明合并示例:

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

等同于:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

除了这些合并外,你还需要了解非导出成员是如何处理的。 非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员。

下例提供了更清晰的说明:

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, because haveMuscles is not accessible here
    }
}

因为haveMuscles并没有导出,只有animalsHaveMuscles函数共享了原始未合并的命名空间可以访问这个变量。 doAnimalsHaveMuscles函数虽是合并命名空间的一部分,但是访问不了未导出的成员。

命名空间与类和函数和枚举类型合并

命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。 TypeScript使用这个功能去实现一些JavaScript里的设计模式。

合并命名空间和类

这让我们可以表示内部类。

class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}

合并规则与上面合并命名空间小节里讲的规则一致,我们必须导出AlbumLabel类,好让合并的类能访问。 合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性。

除了内部类的模式,你在JavaScript里,创建一个函数稍后扩展它增加一些属性也是很常见的。 TypeScript使用声明合并来达到这个目的并保证类型安全。

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

相似的,命名空间可以用来扩展枚举型:

enum Color {
    red = 1,
    green = 2,
    blue = 4
}

namespace Color {
    export function mixColor(colorName: string) {
        if (colorName == "yellow") {
            return Color.red + Color.green;
        }
        else if (colorName == "white") {
            return Color.red + Color.green + Color.blue;
        }
        else if (colorName == "magenta") {
            return Color.red + Color.blue;
        }
        else if (colorName == "cyan") {
            return Color.green + Color.blue;
        }
    }
}

非法的合并

TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并。 想要了解如何模仿类的合并,请参考TypeScript的混入

模块扩展

虽然JavaScript不支持合并,但你可以为导入的对象打补丁以更新它们。让我们考察一下这个玩具性的示例:

// observable.ts
export class Observable<T> {
    // ... implementation left as an exercise for the reader ...
}

// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}

它也可以很好地工作在TypeScript中, 但编译器对 Observable.prototype.map一无所知。 你可以使用扩展模块来将它告诉编译器:

// observable.ts
export class Observable<T> {
    // ... implementation left as an exercise for the reader ...
}

// map.ts
import { Observable } from "./observable";
declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

模块名的解析和用import/export解析模块标识符的方式是一致的。 更多信息请参考 Modules。 当这些声明在扩展中合并时,就如同在原始位置被声明一样。 但是,有两点限制需要注意:

  1. 你不能在扩展中声明新的顶级声明-仅可以扩展模块中已经存在的声明。
  2. 默认导出也不能扩展,只有命名的导出才可以(因为你需要使用导出的名字来进行扩展,并且default是保留关键字 - 详情查看#14080

全局扩展

你也以在模块内部添加声明到全局作用域中。

// observable.ts
export class Observable<T> {
    // ... still no implementation ...
}

declare global {
    interface Array<T> {
        toObservable(): Observable<T>;
    }
}

Array.prototype.toObservable = function () {
    // ...
}

全局扩展与模块扩展的行为和限制是相同的。

Iterators 和 Generators

当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。 一些内置的类型如ArrayMapSetStringInt32ArrayUint32Array等都已经实现了各自的Symbol.iterator。 对象上的Symbol.iterator函数负责返回供迭代的值。

for..of 语句

for..of会遍历可迭代的对象,调用对象上的Symbol.iterator方法。 下面是在数组上使用for..of的简单例子:

let someArray = [1, "string", false];

for (let entry of someArray) {
    console.log(entry); // 1, "string", false
}

for..of vs. for..in 语句

for..offor..in均可迭代一个列表;但是用于迭代的值却不同,for..in迭代的是对象的 的列表,而for..of则迭代对象的键对应的值。

下面的例子展示了两者之间的区别:

let list = [4, 5, 6];

for (let i in list) {
    console.log(i); // "0", "1", "2",
}

for (let i of list) {
    console.log(i); // "4", "5", "6"
}

另一个区别是for..in可以操作任何对象;它提供了查看对象属性的一种方法。 但是for..of关注于迭代对象的值。内置对象MapSet已经实现了Symbol.iterator方法,让我们可以访问它们保存的值。

let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";

for (let pet in pets) {
    console.log(pet); // "species"
}

for (let pet of pets) {
    console.log(pet); // "Cat", "Dog", "Hamster"
}

代码生成

目标为 ES5 和 ES3

当生成目标为ES5或ES3,迭代器只允许在Array类型上使用。 在非数组值上使用for..of语句会得到一个错误,就算这些非数组值已经实现了Symbol.iterator属性。

编译器会生成一个简单的for循环做为for..of循环,比如:

let numbers = [1, 2, 3];
for (let num of numbers) {
    console.log(num);
}

生成的代码为:

var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
    var num = numbers[_i];
    console.log(num);
}

目标为 ECMAScript 2015 或更高

当目标为兼容ECMAScipt 2015的引擎时,编译器会生成相应引擎的for..of内置迭代器实现方式。

JSX

介绍

JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。 JSX因React框架而流行,但也存在其它的实现。 TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript。

基本用法

想要使用JSX必须做两件事:

  1. 给文件一个.tsx扩展名
  2. 启用jsx选项

TypeScript具有三种JSX模式:preservereactreact-native。 这些模式只在代码生成阶段起作用 - 类型检查并不受影响。 在preserve模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有.jsx扩展名。 react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.jsreact-native相当于preserve,它也保留了所有的JSX,但是输出文件的扩展名是.js

模式输入输出输出文件扩展名
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

你可以通过在命令行里使用--jsx标记或tsconfig.json里的选项来指定模式。

*注意:当输出目标为react JSX时,你可以使用--jsxFactory指定JSX工厂函数(默认值为React.createElement

as操作符

回想一下怎么写类型断言:

var foo = <foo>bar;

这里断言bar变量是foo类型的。 因为TypeScript也使用尖括号来表示类型断言,在结合JSX的语法后将带来解析上的困难。因此,TypeScript在.tsx文件里禁用了使用尖括号的类型断言。

由于不能够在.tsx文件里使用上述语法,因此我们应该使用另一个类型断言操作符:as。 上面的例子可以很容易地使用as操作符改写:

var foo = bar as foo;

as操作符在.ts.tsx里都可用,并且与尖括号类型断言行为是等价的。

类型检查

为了理解JSX的类型检查,你必须首先理解固有元素与基于值的元素之间的区别。 假设有这样一个JSX表达式<expr />expr可能引用环境自带的某些东西(比如,在DOM环境里的divspan)或者是你自定义的组件。 这是非常重要的,原因有如下两点:

  1. 对于React,固有元素会生成字符串(React.createElement("div")),然而由你自定义的组件却不会生成(React.createElement(MyComponent))。

  2. 传入JSX元素里的属性类型的查找方式不同。

    固有元素属性_本身_就支持,然而自定义的组件会自己去指定它们具有哪个属性。

TypeScript使用与React相同的规范 来区别它们。 固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。

固有元素

固有元素使用特殊的接口JSX.IntrinsicElements来查找。 默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。 然而,如果这个接口存在,那么固有元素的名字需要在JSX.IntrinsicElements接口的属性里查找。 例如:

declare namespace JSX {
    interface IntrinsicElements {
        foo: any
    }
}

<foo />; // 正确
<bar />; // 错误

在上例中,<foo />没有问题,但是<bar />会报错,因为它没在JSX.IntrinsicElements里指定。

注意:你也可以在JSX.IntrinsicElements上指定一个用来捕获所有字符串索引:

declare namespace JSX {
    interface IntrinsicElements {
        [elemName: string]: any;
    }
}

基于值的元素

基于值的元素会简单的在它所在的作用域里按标识符查找。

import MyComponent from "./myComponent";

<MyComponent />; // 正确
<SomeOtherComponent />; // 错误

有两种方式可以定义基于值的元素:

  1. 函数组件 (FC)
  2. 类组件

由于这两种基于值的元素在JSX表达式里无法区分,因此TypeScript首先会尝试将表达式做为函数组件进行解析。如果解析成功,那么TypeScript就完成了表达式到其声明的解析操作。如果按照函数组件解析失败,那么TypeScript会继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。

函数组件

正如其名,组件被定义成JavaScript函数,它的第一个参数是props对象。 TypeScript会强制它的返回值可以赋值给JSX.Element

interface FooProp {
  name: string;
  X: number;
  Y: number;
}

declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
  return <AnotherComponent name={prop.name} />;
}

const Button = (prop: {value: string}, context: { color: string }) => <button>

由于函数组件是简单的JavaScript函数,所以我们还可以利用函数重载。

interface ClickableProps {
  children: JSX.Element[] | JSX.Element
}

interface HomeProps extends ClickableProps {
  home: JSX.Element;
}

interface SideProps extends ClickableProps {
  side: JSX.Element | string;
}

function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
  ...
}

注意:函数组件之前叫做无状态函数组件(SFC)。由于在当前React版本里,函数组件不再被当作是无状态的,因此类型SFC和它的别名StatelessComponent被废弃了。

类组件

我们可以定义类组件的类型。 然而,我们首先最好弄懂两个新的术语:元素类的类型_和_元素实例的类型

现在有<Expr />,_元素类的类型_为Expr的类型。 所以在上面的例子里,如果MyComponent是ES6的类,那么类类型就是类的构造函数和静态部分。 如果MyComponent是个工厂函数,类类型为这个函数。

一旦建立起了类类型,实例类型由类构造器或调用签名(如果存在的话)的返回值的联合构成。 再次说明,在ES6类的情况下,实例类型为这个类的实例的类型,并且如果是工厂函数,实例类型为这个函数返回值类型。

class MyComponent {
  render() {}
}

// 使用构造签名
var myComponent = new MyComponent();

// 元素类的类型 => MyComponent
// 元素实例的类型 => { render: () => void }

function MyFactoryFunction() {
  return {
    render: () => {
    }
  }
}

// 使用调用签名
var myComponent = MyFactoryFunction();

// 元素类的类型 => MyFactoryFunction
// 元素实例的类型 => { render: () => void }

元素的实例类型很有趣,因为它必须赋值给JSX.ElementClass或抛出一个错误。 默认的JSX.ElementClass{},但是它可以被扩展用来限制JSX的类型以符合相应的接口。

declare namespace JSX {
  interface ElementClass {
    render: any;
  }
}

class MyComponent {
  render() {}
}
function MyFactoryFunction() {
  return { render: () => {} }
}

<MyComponent />; // 正确
<MyFactoryFunction />; // 正确

class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}

<NotAValidComponent />; // 错误
<NotAValidFactoryFunction />; // 错误

属性类型检查

属性类型检查的第一步是确定_元素属性类型_。 这在固有元素和基于值的元素之间稍有不同。

对于固有元素,这是JSX.IntrinsicElements属性的类型。

declare namespace JSX {
  interface IntrinsicElements {
    foo: { bar?: boolean }
  }
}

// `foo`的元素属性类型为`{bar?: boolean}`
<foo bar />;

对于基于值的元素,就稍微复杂些。 它取决于先前确定的在元素实例类型上的某个属性的类型。 至于该使用哪个属性来确定类型取决于JSX.ElementAttributesProperty。 它应该使用单一的属性来定义。 这个属性名之后会被使用。 TypeScript 2.8,如果未指定JSX.ElementAttributesProperty,那么将使用类元素构造函数或函数组件调用的第一个参数的类型。

declare namespace JSX {
  interface ElementAttributesProperty {
    props; // 指定用来使用的属性名
  }
}

class MyComponent {
  // 在元素实例类型上指定属性
  props: {
    foo?: string;
  }
}

// `MyComponent`的元素属性类型为`{foo?: string}`
<MyComponent foo="bar" />

元素属性类型用于的JSX里进行属性的类型检查。 支持可选属性和必须属性。

declare namespace JSX {
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number }
  }
}

<foo requiredProp="bar" />; // 正确
<foo requiredProp="bar" optionalProp={0} />; // 正确
<foo />; // 错误, 缺少 requiredProp
<foo requiredProp={0} />; // 错误, requiredProp 应该是字符串
<foo requiredProp="bar" unknownProp />; // 错误, unknownProp 不存在
<foo requiredProp="bar" some-unknown-prop />; // 正确, `some-unknown-prop`不是个合法的标识符

注意:如果一个属性名不是个合法的JS标识符(像data-*属性),并且它没出现在元素属性类型里时不会当做一个错误。

另外,JSX还会使用JSX.IntrinsicAttributes接口来指定额外的属性,这些额外的属性通常不会被组件的props或arguments使用 - 比如React里的key。还有,JSX.IntrinsicClassAttributes<T>泛型类型也可以用来为类组件(非函数组件)指定相同种类的额外属性。这里的泛型参数表示类实例类型。在React里,它用来允许Ref<T>类型上的ref属性。通常来讲,这些接口上的所有属性都是可选的,除非你想要用户在每个JSX标签上都提供一些属性。

延展操作符也可以使用:

var props = { requiredProp: 'bar' };
<foo {...props} />; // 正确

var badProps = {};
<foo {...badProps} />; // 错误

子孙类型检查

从TypeScript 2.3开始,我们引入了_children_类型检查。_children_是_元素属性(attribute)类型_的一个特殊属性(property),子_JSXExpression_将会被插入到属性里。 与使用JSX.ElementAttributesProperty来决定_props_名类似,我们可以利用JSX.ElementChildrenAttribute来决定_children_名。 JSX.ElementChildrenAttribute应该被声明在单一的属性(property)里。

declare namespace JSX {
  interface ElementChildrenAttribute {
    children: {};  // specify children name to use
  }
}

如不特殊指定子孙的类型,我们将使用React typings里的默认类型。

<div>
  <h1>Hello</h1>
</div>;

<div>
  <h1>Hello</h1>
  World
</div>;

const CustomComp = (props) => <div>{props.children}</div>
<CustomComp>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</CustomComp>
interface PropsType {
  children: JSX.Element
  name: string
}

class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

// OK
<Component name="foo">
  <h1>Hello World</h1>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element
<Component name="bar">
  <h1>Hello World</h1>
  <h2>Hello World</h2>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element or string.
<Component name="baz">
  <h1>Hello</h1>
  World
</Component>

JSX结果类型

默认地JSX表达式结果的类型为any。 你可以自定义这个类型,通过指定JSX.Element接口。 然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。 它是一个黑盒。

嵌入的表达式

JSX允许你使用{ }标签来内嵌表达式。

var a = <div>
  {['foo', 'bar'].map(i => <span>{i / 2}</span>)}
</div>

上面的代码产生一个错误,因为你不能用数字来除以一个字符串。 输出如下,若你使用了preserve选项:

var a = <div>
  {['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React整合

要想一起使用JSX和React,你应该使用React类型定义。 这些类型声明定义了JSX合适命名空间来使用React。

/// <reference path="react.d.ts" />

interface Props {
  foo: string;
}

class MyComponent extends React.Component<Props, {}> {
  render() {
    return <span>{this.props.foo}</span>
  }
}

<MyComponent foo="bar" />; // 正确
<MyComponent foo={0} />; // 错误

工厂函数

jsx: react编译选项使用的工厂函数是可以配置的。可以使用jsxFactory命令行选项,或内联的@jsx注释指令在每个文件上设置。比如,给createElement设置jsxFactory<div />会使用createElement("div")来生成,而不是React.createElement("div")

注释指令可以像下面这样使用(在TypeScript 2.8里):

import preact = require("preact");
/* @jsx preact.h */
const x = <div />;

生成:

const preact = require("preact");
const x = preact.h("div", null);

工厂函数的选择同样会影响JSX命名空间的查找(类型检查)。如果工厂函数使用React.createElement定义(默认),编译器会先检查React.JSX,之后才检查全局的JSX。如果工厂函数定义为h,那么在检查全局的JSX之前先检查h.JSX

混入

Table of contents

介绍

混入示例

理解示例

介绍

↥ 回到顶端

除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 你可能在Scala等语言里对mixins及其特性已经很熟悉了,但它在JavaScript中也是很流行的。

混入示例

↥ 回到顶端

下面的代码演示了如何在TypeScript里使用混入。 后面我们还会解释这段代码是怎么工作的。

// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

class SmartObject {
    constructor() {
        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
    }

    interact() {
        this.activate();
    }
}

interface SmartObject extends Disposable, Activatable {}
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
        });
    });
}

理解示例

↥ 回到顶端

代码里首先定义了两个类,它们将做为mixins。 可以看到每个类都只定义了一个特定的行为或功能。 稍后我们使用它们来创建一个新类,同时具有这两种功能。

// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

下面创建一个类,结合了这两个mixins。 下面来看一下具体是怎么操作的:

class SmartObject {
    ...
}

interface SmartObject extends Disposable, Activatable {}

首先注意到的是,我们没有在SmartObject类里面继承DisposableActivatable,而是在SmartObject接口里面继承的。由于声明合并的存在,SmartObject接口会被混入到SmartObject类里面。

它将类视为接口,且只会混入Disposable和Activatable背后的类型到SmartObject类型里,不会混入实现。也就是说,我们要在类里面去实现。 这正是我们想要在混入时避免的行为。

最后,我们将混入融入到了类的实现中去。

// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;

最后,把mixins混入定义的类,完成全部实现部分。

applyMixins(SmartObject, [Disposable, Activatable]);

最后,创建这个帮助函数,帮我们做混入操作。 它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
        })
    });
}

模块

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。

模块在其自身的作用域里执行,而不是在全局作用域里;这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们。 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用import形式之一。

模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。

模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 众所周知的JavaScript模块加载器有:作用于CommonJS模块的Node.js加载器和在Web应用里作用于AMD模块的RequireJS加载器。

TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。 相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

导出

导出声明

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。

StringValidator.ts

export interface StringValidator {
    isAcceptable(s: string): boolean;
}

ZipCodeValidator.ts

import { StringValidator } from "./StringValidator";

export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

导出语句

导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };

重新导出

我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。

ParseIntBasedZipCodeValidator.ts

export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"

AllValidators.ts

export * from "./StringValidator"; // exports 'StringValidator' interface
export * from "./ZipCodeValidator";  // exports 'ZipCodeValidator' and const 'numberRegexp' class
export * from "./ParseIntBasedZipCodeValidator"; //  exports the 'ParseIntBasedZipCodeValidator' class
                                                 // and re-exports 'RegExpBasedZipCodeValidator' as alias
                                                 // of the 'ZipCodeValidator' class from 'ZipCodeValidator.ts'

导入

模块的导入操作与导出一样简单。 可以使用以下import形式之一来导入其它模块中的导出内容。

导入一个模块中的某个导出内容

import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();

可以对导入内容重命名

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

将整个模块导入到一个变量,并通过它来访问模块的导出部分

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

具有副作用的导入模块

尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:

import "./my-module.js";

默认导出

每个模块都可以有一个default导出。 默认导出使用default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入default导出。

default导出十分便利。 比如,像jQuery这样的类库可能有一个默认导出jQuery$,并且我们基本上也会使用同样的名字jQuery$导出jQuery。

jQuery.d.ts

declare let $: jQuery;
export default $;

App.ts

import $ from "jQuery";

$("button.continue").html( "Next Step..." );

类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。

ZipCodeValidator.ts

export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}

Test.ts

import validator from "./ZipCodeValidator";

let myValidator = new validator();

或者

StaticZipCodeValidator.ts

const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}

Test.ts

import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// Use function validate
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? "matches" : "does not match"}`);
});

default导出也可以是一个值

OneTwoThree.ts

export default "123";

Log.ts

import num from "./OneTwoThree";

console.log(num); // "123"

export =import = require()

CommonJS和AMD的环境里都有一个exports变量,这个变量包含了一个模块的所有导出内容。

CommonJS和AMD的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。虽然作用相似,但是 export default 语法并不能兼容CommonJS和AMD的exports

为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。

export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。

若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。

ZipCodeValidator.ts

let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;

Test.ts

import zip = require("./ZipCodeValidator");

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validator = new zip();

// Show whether each string passed each validator
strings.forEach(s => {
  console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

生成模块代码

根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),UMD, SystemJSECMAScript 2015 native modules (ES6)模块加载系统使用的代码。 想要了解生成代码中definerequireregister的意义,请参考相应模块加载器的文档。

下面的例子说明了导入导出语句里使用的名字是怎么转换为相应的模块加载器代码的。

SimpleModule.ts

import m = require("mod");
export let t = m.something + 1;

AMD / RequireJS SimpleModule.js

define(["require", "exports", "./mod"], function (require, exports, mod_1) {
    exports.t = mod_1.something + 1;
});

CommonJS / Node SimpleModule.js

let mod_1 = require("./mod");
exports.t = mod_1.something + 1;

UMD SimpleModule.js

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        let v = factory(require, exports); if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./mod"], factory);
    }
})(function (require, exports) {
    let mod_1 = require("./mod");
    exports.t = mod_1.something + 1;
});

System SimpleModule.js

System.register(["./mod"], function(exports_1) {
    let mod_1;
    let t;
    return {
        setters:[
            function (mod_1_1) {
                mod_1 = mod_1_1;
            }],
        execute: function() {
            exports_1("t", t = mod_1.something + 1);
        }
    }
});

Native ECMAScript 2015 modules SimpleModule.js

import { something } from "./mod";
export let t = something + 1;

简单示例

下面我们来整理一下前面的验证器实现,每个模块只有一个命名的导出。

为了编译,我们必需要在命令行上指定一个模块目标。对于Node.js来说,使用--module commonjs; 对于Require.js来说,使用--module amd。比如:

tsc --module commonjs Test.ts

编译完成后,每个模块会生成一个单独的.js文件。 好比使用了reference标签,编译器会根据import语句编译相应的文件。

Validation.ts

export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts

import { StringValidator } from "./Validation";

const lettersRegexp = /^[A-Za-z]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

ZipCodeValidator.ts

import { StringValidator } from "./Validation";

const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

Test.ts

import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
strings.forEach(s => {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
});

可选的模块加载和其它高级加载场景

有时候,你只想在某种条件下才加载某个模块。 在TypeScript里,使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。

编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成require这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。

这种模式的核心是import id = require("...")语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过require),就像下面if代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意import定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。

为了确保类型安全性,我们可以使用typeof关键字。 typeof关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。

示例:Node.js里的动态模块加载

declare function require(moduleName: string): any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
    let validator = new ZipCodeValidator();
    if (validator.isAcceptable("...")) { /* ... */ }
}

示例:require.js里的动态模块加载

declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;

import * as Zip from "./ZipCodeValidator";

if (needZipValidation) {
    require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
        let validator = new ZipCodeValidator.ZipCodeValidator();
        if (validator.isAcceptable("...")) { /* ... */ }
    });
}

示例:System.js里的动态模块加载

declare const System: any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
        var x = new ZipCodeValidator();
        if (x.isAcceptable("...")) { /* ... */ }
    });
}

使用其它的JavaScript库

要想描述非TypeScript编写的类库的类型,我们需要声明类库所暴露出的API。

我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在.d.ts文件里定义的。 如果你熟悉C/C++,你可以把它们当做.h文件。 让我们看一些例子。

外部模块

在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的export声明来为每个模块都定义一个.d.ts文件,但最好还是写在一个大的.d.ts文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用module关键字并且把名字用引号括起来,方便之后import。 例如:

node.d.ts (simplified excerpt)

declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}

现在我们可以/// <reference> node.d.ts并且使用import url = require("url");import * as URL from "url"加载模块。

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

外部模块简写

假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。

declarations.d.ts

declare module "hot-new-module";

简写模块里所有导出的类型将是any

import x, {y} from "hot-new-module";
x(y);

模块声明通配符

某些模块加载器如SystemJSAMD支持导入非JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。

declare module "*!text" {
    const content: string;
    export default content;
}
// Some do it the other way around.
declare module "json!*" {
    const value: any;
    export default value;
}

现在你可以就导入匹配"*!text""json!*"的内容了。

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

UMD模块

有些模块被设计成兼容多个模块加载器,或者不使用模块加载器(全局变量)。 它们以UMD模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。 例如:

math-lib.d.ts

export function isPrime(x: number): boolean;
export as namespace mathLib;

之后,这个库可以在某个模块里通过导入来使用:

import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module

它同样可以通过全局变量的形式使用,但只能在某个脚本里。 (脚本是指一个不带有导入或导出的文件。)

mathLib.isPrime(2);

创建模块结构指导

尽可能地在顶层导出

用户应该更容易地使用你模块导出的内容。 嵌套层次过多会变得难以处理,因此仔细考虑一下如何组织你的代码。

从你的模块中导出一个命名空间就是一个增加嵌套的例子。 虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。 这对用户来说是很不便的并且通常是多余的。

导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。 除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。

如果仅导出单个 classfunction,使用 export default

就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。 如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。 这会令模块的导入和使用变得些许简单。 比如:

MyClass.ts

export default class SomeType {
  constructor() { ... }
}

MyFunc.ts

export default function getThing() { return 'thing'; }

Consumer.ts

import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());

对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t)并且不需要多余的(.)来找到相关对象。

如果要导出多个对象,把它们放在顶层里导出

MyThings.ts

export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

相反地,当导入的时候:

明确地列出导入的名字

Consumer.ts

import { SomeType, SomeFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();

使用命名空间导入模式当你要导出大量内容的时候

MyLargeModule.ts

export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }

Consumer.ts

import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();

使用重新导出进行扩展

你可能经常需要去扩展一个模块的功能。 JS里常用的一个模式是jQuery那样去扩展原对象。 如我们之前提到的,模块不会像全局命名空间对象那样去_合并_。 推荐的方案是_不要_去改变原来的对象,而是导出一个新的实体来提供新的功能。

假设Calculator.ts模块里定义了一个简单的计算器实现。 这个模块同样提供了一个辅助函数来测试计算器的功能,通过传入一系列输入的字符串并在最后给出结果。

Calculator.ts

export class Calculator {
    private current = 0;
    private memory = 0;
    private operator: string;

    protected processDigit(digit: string, currentValue: number) {
        if (digit >= "0" && digit <= "9") {
            return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
        }
    }

    protected processOperator(operator: string) {
        if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
            return operator;
        }
    }

    protected evaluateOperator(operator: string, left: number, right: number): number {
        switch (this.operator) {
            case "+": return left + right;
            case "-": return left - right;
            case "*": return left * right;
            case "/": return left / right;
        }
    }

    private evaluate() {
        if (this.operator) {
            this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
        }
        else {
            this.memory = this.current;
        }
        this.current = 0;
    }

    public handleChar(char: string) {
        if (char === "=") {
            this.evaluate();
            return;
        }
        else {
            let value = this.processDigit(char, this.current);
            if (value !== undefined) {
                this.current = value;
                return;
            }
            else {
                let value = this.processOperator(char);
                if (value !== undefined) {
                    this.evaluate();
                    this.operator = value;
                    return;
                }
            }
        }
        throw new Error(`Unsupported input: '${char}'`);
    }

    public getResult() {
        return this.memory;
    }
}

export function test(c: Calculator, input: string) {
    for (let i = 0; i < input.length; i++) {
        c.handleChar(input[i]);
    }

    console.log(`result of '${input}' is '${c.getResult()}'`);
}

下面使用导出的test函数来测试计算器。

TestCalculator.ts

import { Calculator, test } from "./Calculator";


let c = new Calculator();
test(c, "1+2*33/11="); // prints 9

现在扩展它,添加支持输入其它进制(十进制以外),让我们来创建ProgrammerCalculator.ts

ProgrammerCalculator.ts

import { Calculator } from "./Calculator";

class ProgrammerCalculator extends Calculator {
    static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];

    constructor(public base: number) {
        super();
        const maxBase = ProgrammerCalculator.digits.length;
        if (base <= 0 || base > maxBase) {
            throw new Error(`base has to be within 0 to ${maxBase} inclusive.`);
        }
    }

    protected processDigit(digit: string, currentValue: number) {
        if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
            return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
        }
    }
}

// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };

// Also, export the helper function
export { test } from "./Calculator";

新的ProgrammerCalculator模块导出的API与原先的Calculator模块很相似,但却没有改变原模块里的对象。 下面是测试ProgrammerCalculator类的代码:

TestProgrammerCalculator.ts

import { Calculator, test } from "./ProgrammerCalculator";

let c = new Calculator(2);
test(c, "001+010="); // prints 3

模块里不要使用命名空间

当初次进入基于模块的开发模式时,可能总会控制不住要将导出包裹在一个命名空间里。 模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。 记住这点,命名空间在使用模块时几乎没什么价值。

在组织方面,命名空间对于在全局作用域内对逻辑上相关的对象和类型进行分组是很便利的。 例如,在C#里,你会从System.Collections里找到所有集合的类型。 通过将类型有层次地组织在命名空间里,可以方便用户找到与使用那些类型。 然而,模块本身已经存在于文件系统之中,这是必须的。 我们必须通过路径和文件名找到它们,这已经提供了一种逻辑上的组织形式。 我们可以创建/collections/generic/文件夹,把相应模块放在这里面。

命名空间对解决全局作用域里命名冲突来说是很重要的。 比如,你可以有一个My.Application.Customer.AddFormMy.Application.Order.AddForm -- 两个类型的名字相同,但命名空间不同。 然而,这对于模块来说却不是一个问题。 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。

更多关于模块和命名空间的资料查看命名空间和模块

危险信号

以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:

  • 文件的顶层声明是export namespace Foo { ... } (删除Foo并把所有内容向上层移动一层)©
  • 多个文件的顶层具有同样的export namespace Foo { (不要以为这些会合并到一个Foo中!)

模块解析

这节假设你已经了解了模块的一些基本知识 请阅读模块文档了解更多信息。

_模块解析_是指编译器在查找导入模块内容时所遵循的流程。 假设有一个导入语句import { a } from "moduleA"; 为了去检查任何对a的使用,编译器需要准确的知道它表示什么,并且需要检查它的定义moduleA

这时候,编译器会有个疑问“moduleA的结构是怎样的?” 这听上去很简单,但moduleA可能在你写的某个.ts/.tsx文件里或者在你的代码所依赖的.d.ts里。

首先,编译器会尝试定位表示导入模块的文件。 编译器会遵循以下二种策略之一:ClassicNode。 这些策略会告诉编译器到_哪里_去查找moduleA

如果上面的解析失败了并且模块名是非相对的(且是在"moduleA"的情况下),编译器会尝试定位一个外部模块声明。 我们接下来会讲到非相对导入。

最后,如果编译器还是不能解析这个模块,它会记录一个错误。 在这种情况下,错误可能为error TS2307: Cannot find module 'moduleA'.

相对 vs. 非相对模块导入

根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。

_相对导入_是以/./../开头的。 下面是一些例子:

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

所有其它形式的导入被当作_非相对_的。 下面是一些例子:

  • import * as $ from "jQuery";
  • import { Component } from "@angular/core";

相对导入在解析时是相对于导入它的文件,并且_不能_解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。

非相对模块的导入可以相对于baseUrl或通过下文会讲到的路径映射来进行解析。 它们还可以被解析成外部模块声明。 使用非相对路径来导入你的外部依赖。

模块解析策略

共有两种可用的模块解析策略:NodeClassic。 你可以使用--moduleResolution标记来指定使用哪种模块解析策略。 若未指定,那么在使用了--module AMD | System | ES2015时的默认值为Classic,其它情况时则为Node

Classic

这种策略在以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。

相对导入的模块是相对于导入它的文件进行解析的。 因此/root/src/folder/A.ts文件里的import { b } from "./moduleB"会使用下面的查找流程:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。

比如:

有一个对moduleB的非相对导入import { b } from "moduleB",它是在/root/src/folder/A.ts文件里,会以如下的方式来定位"moduleB"

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

Node

这个解析策略试图在运行时模仿Node.js模块解析机制。 完整的Node.js解析算法可以在Node.js module documentation找到。

Node.js如何解析模块

为了理解TypeScript编译依照的解析步骤,先弄明白Node.js模块是非常重要的。 通常,在Node.js里导入是通过require函数调用进行的。 Node.js会根据require的是相对路径还是非相对路径做出不同的行为。

相对路径很简单。 例如,假设有一个文件路径为/root/src/moduleA.js,包含了一个导入var x = require("./moduleB"); Node.js以下面的顺序解析这个导入:

  1. 检查/root/src/moduleB.js文件是否存在。
  2. 检查/root/src/moduleB目录是否包含一个package.json文件,且package.json文件指定了一个"main"模块。 在我们的例子里,如果Node.js发现文件/root/src/moduleB/package.json包含了{ "main": "lib/mainModule.js" },那么Node.js会引用/root/src/moduleB/lib/mainModule.js
  3. 检查/root/src/moduleB目录是否包含一个index.js文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。

你可以阅读Node.js文档了解更多详细信息:file modulesfolder modules

但是,非相对模块名的解析是个完全不同的过程。 Node会在一个特殊的文件夹node_modules里查找你的模块。 node_modules可能与当前文件在同一级目录下,或者在上层目录里。 Node会向上级目录遍历,查找每个node_modules直到它找到要加载的模块。

还是用上面例子,但假设/root/src/moduleA.js里使用的是非相对路径导入var x = require("moduleB");。 Node则会以下面的顺序去解析moduleB,直到有一个匹配上。

  1. /root/src/node_modules/moduleB.js
  2. /root/src/node_modules/moduleB/package.json (如果指定了"main"属性)
  3. /root/src/node_modules/moduleB/index.js
  4. /root/node_modules/moduleB.js
  5. /root/node_modules/moduleB/package.json (如果指定了"main"属性)
  6. /root/node_modules/moduleB/index.js
  7. /node_modules/moduleB.js
  8. /node_modules/moduleB/package.json (如果指定了"main"属性)
  9. /node_modules/moduleB/index.js

注意Node.js在步骤(4)和(7)会向上跳一级目录。

你可以阅读Node.js文档了解更多详细信息:loading modules from node_modules

TypeScript如何解析模块

TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名(.ts.tsx.d.ts)。 同时,TypeScript在package.json里使用字段"types"来表示类似"main"的意义 - 编译器会使用它来找到要使用的"main"定义文件。

比如,有一个导入语句import { b } from "./moduleB"/root/src/moduleA.ts里,会以下面的流程来定位"./moduleB"

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (如果指定了"types"属性)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

回想一下Node.js先查找moduleB.js文件,然后是合适的package.json,再之后是index.js

类似地,非相对的导入会遵循Node.js的解析逻辑,首先查找文件,然后是合适的文件夹。 因此/root/src/moduleA.ts文件里的import { b } from "moduleB"会以下面的查找顺序解析:

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json (如果指定了"types"属性)
  5. /root/src/node_modules/@types/moduleB.d.ts
  6. /root/src/node_modules/moduleB/index.ts
  7. /root/src/node_modules/moduleB/index.tsx
  8. /root/src/node_modules/moduleB/index.d.ts
  9. /root/node_modules/moduleB.ts
  10. /root/node_modules/moduleB.tsx
  11. /root/node_modules/moduleB.d.ts
  12. /root/node_modules/moduleB/package.json (如果指定了"types"属性)
  13. /root/node_modules/@types/moduleB.d.ts
  14. /root/node_modules/moduleB/index.ts
  15. /root/node_modules/moduleB/index.tsx
  16. /root/node_modules/moduleB/index.d.ts
  17. /node_modules/moduleB.ts
  18. /node_modules/moduleB.tsx
  19. /node_modules/moduleB.d.ts
  20. /node_modules/moduleB/package.json (如果指定了"types"属性)
  21. /node_modules/@types/moduleB.d.ts
  22. /node_modules/moduleB/index.ts
  23. /node_modules/moduleB/index.tsx
  24. /node_modules/moduleB/index.d.ts

不要被这里步骤的数量吓到 - TypeScript只是在步骤(9)和(17)向上跳了两次目录。 这并不比Node.js里的流程复杂。

附加的模块解析标记

有时工程源码结构与输出结构不同。 通常是要经过一系统的构建步骤最后生成输出。 它们包括将.ts编译成.js,将不同位置的依赖拷贝至一个输出位置。 最终结果就是运行时的模块名与包含它们声明的源文件里的模块名不同。 或者最终输出文件里的模块路径与编译时的源文件路径不同了。

TypeScript编译器有一些额外的标记用来_通知_编译器在源码编译成最终输出的过程中都发生了哪个转换。

有一点要特别注意的是编译器_不会_进行这些转换操作; 它只是利用这些信息来指导模块的导入。

Base URL

在利用AMD模块加载器的应用里使用baseUrl是常见做法,它要求在运行时模块都被放到了一个文件夹里。 这些模块的源码可以在不同的目录下,但是构建脚本会将它们集中到一起。

设置baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于baseUrl

_baseUrl_的值由以下两者之一决定:

  • 命令行中_baseUrl_的值(如果给定的路径是相对的,那么将相对于当前路径进行计算)
  • ‘tsconfig.json’里的_baseUrl_属性(如果给定的路径是相对的,那么将相对于‘tsconfig.json’路径进行计算)

注意相对模块的导入不会被设置的baseUrl所影响,因为它们总是相对于导入它们的文件。

阅读更多关于baseUrl的信息RequireJSSystemJS

路径映射

有时模块不是直接放在_baseUrl_下面。 比如,充分"jquery"模块地导入,在运行时可能被解释为"node_modules/jquery/dist/jquery.slim.min.js"。 加载器使用映射配置来将模块名映射到运行时的文件,查看RequireJs documentationSystemJS documentation

TypeScript编译器通过使用tsconfig.json文件里的"paths"来支持这样的声明映射。 下面是一个如何指定jquery"paths"的例子。

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // 此处映射是相对于"baseUrl"
    }
  }
}

请注意"paths"是相对于"baseUrl"进行解析。 如果"baseUrl"被设置成了除"."外的其它值,比如tsconfig.json所在的目录,那么映射必须要做相应的改变。 如果你在上例中设置了"baseUrl": "./src",那么jquery应该映射到"../node_modules/jquery/dist/jquery"

通过"paths"我们还可以指定复杂的映射,包括指定多个回退位置。 假设在一个工程配置里,有一些模块位于一处,而其它的则在另个的位置。 构建过程会将它们集中至一处。 工程结构可能如下:

projectRoot
├── folder1
│   ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│   └── file2.ts
├── generated
│   ├── folder1
│   └── folder2
│       └── file3.ts
└── tsconfig.json

相应的tsconfig.json文件如下:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": [
        "*",
        "generated/*"
      ]
    }
  }
}

它告诉编译器所有匹配"*"(所有的值)模式的模块导入会在以下两个位置查找:

  1. "*": 表示名字不发生改变,所以映射为<moduleName> => <baseUrl>/<moduleName>
  2. "generated/*"表示模块名添加了“generated”前缀,所以映射为<moduleName> => <baseUrl>/generated/<moduleName>

按照这个逻辑,编译器将会如下尝试解析这两个导入:

  • 导入'folder1/file2'
    1. 匹配'*'模式且通配符捕获到整个名字。
    2. 尝试列表里的第一个替换:'*' -> folder1/file2
    3. 替换结果为非相对名 - 与_baseUrl_合并 -> projectRoot/folder1/file2.ts
    4. 文件存在。完成。
  • 导入'folder2/file3'
    1. 匹配'*'模式且通配符捕获到整个名字。
    2. 尝试列表里的第一个替换:'*' -> folder2/file3
    3. 替换结果为非相对名 - 与_baseUrl_合并 -> projectRoot/folder2/file3.ts
    4. 文件不存在,跳到第二个替换。
    5. 第二个替换:'generated/*' -> generated/folder2/file3
    6. 替换结果为非相对名 - 与_baseUrl_合并 -> projectRoot/generated/folder2/file3.ts
    7. 文件存在。完成。

利用rootDirs指定虚拟目录

有时多个目录下的工程源文件在编译时会进行合并放在某个输出目录下。 这可以看做一些源目录创建了一个“虚拟”目录。

利用rootDirs,可以告诉编译器生成这个虚拟目录的_roots_; 因此编译器可以在“虚拟”目录下解析相对模块导入,就_好像_它们被合并在了一起一样。

比如,有下面的工程结构:

 src
 └── views
     └── view1.ts (imports './template1')
     └── view2.ts

 generated
 └── templates
         └── views
             └── template1.ts (imports './view2')

src/views里的文件是用于控制UI的用户代码。 generated/templates是UI模版,在构建时通过模版生成器自动生成。 构建中的一步会将/src/views/generated/templates/views的输出拷贝到同一个目录下。 在运行时,视图可以假设它的模版与它同在一个目录下,因此可以使用相对导入"./template"

可以使用"rootDirs"来告诉编译器。 "rootDirs"指定了一个_roots_列表,列表里的内容会在运行时被合并。 因此,针对这个例子,tsconfig.json如下:

{
  "compilerOptions": {
    "rootDirs": [
      "src/views",
      "generated/templates/views"
    ]
  }
}

每当编译器在某一rootDirs的子目录下发现了相对模块导入,它就会尝试从每一个rootDirs中导入。

rootDirs的灵活性不仅仅局限于其指定了要在逻辑上合并的物理目录列表。它提供的数组可以包含任意数量的任何名字的目录,不论它们是否存在。这允许编译器以类型安全的方式处理复杂捆绑(bundles)和运行时的特性,比如条件引入和工程特定的加载器插件。

设想这样一个国际化的场景,构建工具自动插入特定的路径记号来生成针对不同区域的捆绑,比如将#{locale}做为相对模块路径./#{locale}/messages的一部分。在这个假定的设置下,工具会枚举支持的区域,将抽像的路径映射成./zh/messages./de/messages等。

假设每个模块都会导出一个字符串的数组。比如./zh/messages可能包含:

export default [
    "您好吗",
    "很高兴认识你"
];

利用rootDirs我们可以让编译器了解这个映射关系,从而也允许编译器能够安全地解析./#{locale}/messages,就算这个目录永远都不存在。比如,使用下面的tsconfig.json

{
  "compilerOptions": {
    "rootDirs": [
      "src/zh",
      "src/de",
      "src/#{locale}"
    ]
  }
}

编译器现在可以将import messages from './#{locale}/messages'解析为import messages from './zh/messages'用做工具支持的目的,并允许在开发时不必了解区域信息。

跟踪模块解析

如之前讨论,编译器在解析模块时可能访问当前文件夹外的文件。 这会导致很难诊断模块为什么没有被解析,或解析到了错误的位置。 通过--traceResolution启用编译器的模块解析跟踪,它会告诉我们在模块解析过程中发生了什么。

假设我们有一个使用了typescript模块的简单应用。 app.ts里有一个这样的导入import * as ts from "typescript"

│   tsconfig.json
├───node_modules
│   └───typescript
│       └───lib
│               typescript.d.ts
└───src
        app.ts

使用--traceResolution调用编译器。

tsc --traceResolution

输出结果如下:

======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

需要留意的地方

  • 导入的名字及位置

    ======== Resolving module 'typescript' from 'src/app.ts'. ========

  • 编译器使用的策略

    Module resolution kind is not specified, using 'NodeJs'.

  • 从npm加载types

    'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.

  • 最终结果

    ======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

使用--noResolve

正常来讲编译器会在开始编译之前解析模块导入。 每当它成功地解析了对一个文件import,这个文件被会加到一个文件列表里,以供编译器稍后处理。

--noResolve编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。

比如

app.ts

import * as A from "moduleA" // OK, moduleA passed on the command-line
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve

使用--noResolve编译app.ts

  • 可能正确找到moduleA,因为它在命令行上指定了。
  • 找不到moduleB,因为没有在命令行上传递。

常见问题

为什么在exclude列表里的模块还会被编译器使用

tsconfig.json将文件夹转变一个“工程” 如果不指定任何“exclude”“files”,文件夹里的所有文件包括tsconfig.json和所有的子目录都会在编译列表里。 如果你想利用“exclude”排除某些文件,甚至你想指定所有要编译的文件列表,请使用“files”

有些是被tsconfig.json自动加入的。 它不会涉及到上面讨论的模块解析。 如果编译器识别出一个文件是模块导入目标,它就会加到编译列表里,不管它是否被排除了。

因此,要从编译列表中排除一个文件,你需要在排除它的同时,还要排除所有对它进行import或使用了/// <reference path="..." />指令的文件。

命名空间

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

这篇文章描述了如何在TypeScript里使用命名空间(之前叫做“内部模块”)来组织你的代码。 就像我们在术语说明里提到的那样,“内部模块”现在叫做“命名空间”。 另外,任何使用module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。 这就避免了让新的使用者被相似的名称所迷惑。

第一步

我们先来写一段程序并将在整篇文章中都使用这个例子。 我们定义几个简单的字符串验证器,假设你会使用它们来验证表单里的用户输入或验证外部数据。

所有的验证器都放在一个文件里

interface StringValidator {
    isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
    }
}

命名空间

随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突。 因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下。

下面的例子里,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用export。 相反的,变量lettersRegexpnumberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。 在文件末尾的测试代码里,由于是在命名空间之外访问,因此需要限定类型的名称,比如Validation.LettersOnlyValidator

使用命名空间的验证器

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

分离到多文件

当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。

多文件中的命名空间

现在,我们把Validation命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。 我们的测试代码保持不变。

Validation.ts

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}

LettersOnlyValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
    const lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}

ZipCodeValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
    const numberRegexp = /^[0-9]+$/;
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

Test.ts

/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

当涉及到多文件时,我们必须确保所有编译后的代码都被加载了。 我们有两种方式。

第一种方式,把所有的输入文件编译为一个输出文件,需要使用--outFile标记:

tsc --outFile sample.js Test.ts

编译器会根据源码里的引用标签自动地对输出进行排序。你也可以单独地指定每个文件。

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

第二种方式,我们可以编译每一个文件(默认方式),那么每个源文件都会对应生成一个JavaScript文件。 然后,在页面上通过<script>标签把所有生成的JavaScript文件按正确的顺序引进来,比如:

MyTestPage.html (excerpt)

    <script src="Validation.js" type="text/javascript" />
    <script src="LettersOnlyValidator.js" type="text/javascript" />
    <script src="ZipCodeValidator.js" type="text/javascript" />
    <script src="Test.js" type="text/javascript" />

别名

另一种简化命名空间操作的方法是使用import q = x.y.z给常用的对象起一个短的名字。 不要与用来加载模块的import x = require('name')语法弄混了,这里的语法是为指定的符号创建一个别名。 你可以用这种方法为任意标识符创建别名,也包括导入的模块中的对象。

namespace Shapes {
    export namespace Polygons {
        export class Triangle { }
        export class Square { }
    }
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"

注意,我们并没有使用require关键字,而是直接使用导入符号的限定名赋值。 这与使用var相似,但它还适用于类型和导入的具有命名空间含义的符号。 重要的是,对于值来讲,import会生成与原始符号不同的引用,所以改变别名的var值并不会影响原始变量的值。

使用其它的JavaScript库

为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。 由于大部分程序库只提供少数的顶级对象,命名空间是用来表示它们的一个好办法。

我们称其为声明是因为它不是外部程序的具体实现。 我们通常在.d.ts里写这些声明。 如果你熟悉C/C++,你可以把它们当做.h文件。 让我们看一些例子。

外部命名空间

流行的程序库D3在全局对象d3里定义它的功能。 因为这个库通过一个<script>标签加载(不是通过模块加载器),它的声明文件使用内部模块来定义它的类型。 为了让TypeScript编译器识别它的类型,我们使用外部命名空间声明。 比如,我们可以像下面这样写:

D3.d.ts (部分摘录)

declare namespace D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        };
    }

    export interface Event {
        x: number;
        y: number;
    }

    export interface Base extends Selectors {
        event: Event;
    }
}

declare var d3: D3.Base;

命名空间和模块

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

这篇文章将概括介绍在TypeScript里使用模块与命名空间来组织代码的方法。 我们也会谈及命名空间和模块的高级使用场景,和在使用它们的过程中常见的陷阱。

查看模块章节了解关于模块的更多信息。 查看命名空间章节了解关于命名空间的更多信息。

使用命名空间

命名空间是位于全局命名空间下的一个普通的带有名字的JavaScript对象。 这令命名空间十分容易使用。 它们可以在多文件中同时使用,并通过--outFile结合在一起。 命名空间是帮你组织Web应用不错的方式,你可以把所有依赖都放在HTML页面的<script>标签里。

但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。

使用模块

像命名空间一样,模块可以包含代码和声明。 不同的是模块可以_声明_它的依赖。

模块会把依赖添加到模块加载器上(例如CommonJs / Require.js)。 对于小型的JS应用来说可能没必要,但是对于大型应用,这一点点的花费会带来长久的模块化和可维护性上的便利。 模块也提供了更好的代码重用,更强的封闭性以及更好的使用工具进行优化。

对于Node.js应用来说,模块是默认并推荐的组织代码的方式。

从ECMAScript 2015开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。 因此,对于新项目来说推荐使用模块做为组织代码的方式。

命名空间和模块的陷阱

这部分我们会描述常见的命名空间和模块的使用陷阱和如何去避免它们。

对模块使用/// <reference>

一个常见的错误是使用/// <reference>引用模块文件,应该使用import。 要理解这之间的区别,我们首先应该弄清编译器是如何根据import路径(例如,import x from "...";import x = require("...")里面的...,等等)来定位模块的类型信息的。

编译器首先尝试去查找相应路径下的.ts.tsx再或者.d.ts。 如果这些文件都找不到,编译器会查找_外部模块声明_。 回想一下,它们是在.d.ts文件里声明的。

  • myModules.d.ts
// In a .d.ts file or .ts file that is not a module:
declare module "SomeModule" {
    export function fn(): string;
}
  • myOtherModule.ts
/// <reference path="myModules.d.ts" />
import * as m from "SomeModule";

这里的引用标签指定了外来模块的位置。 这就是一些TypeScript例子中引用node.d.ts的方法。

不必要的命名空间

如果你想把命名空间转换为模块,它可能会像下面这个文件:

  • shapes.ts
export namespace Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
}

顶层的模块Shapes包裹了TriangleSquare。 对于使用它的人来说这是令人迷惑和讨厌的:

  • shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?

TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。 因为使用模块的人会为它们命名,所以完全没有必要把导出的符号包裹在一个命名空间里。

再次重申,不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。 模块文件本身已经是一个逻辑分组,并且它的名字是由导入这个模块的代码指定,所以没有必要为导出的对象增加额外的模块层。

下面是改进的例子:

  • shapes.ts
export class Triangle { /* ... */ }
export class Square { /* ... */ }
  • shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();

模块的取舍

就像每个JS文件对应一个模块一样,TypeScript里模块文件与生成的JS文件也是一一对应的。 这会产生一种影响,根据你指定的目标模块系统的不同,你可能无法连接多个模块源文件。 例如当目标模块系统为commonjsumd时,无法使用outFile选项,但是在TypeScript 1.8以上的版本能够使用outFile当目标为amdsystem

Symbols

介绍

自ECMAScript 2015起,symbol成为了一种新的原生类型,就像numberstring一样。

symbol类型的值是通过Symbol构造函数创建的。

let sym1 = Symbol();

let sym2 = Symbol("key"); // 可选的字符串key

Symbols是不可改变且唯一的。

let sym2 = Symbol("key");
let sym3 = Symbol("key");

sym2 === sym3; // false, symbols是唯一的

像字符串一样,symbols也可以被用做对象属性的键。

const sym = Symbol();

let obj = {
    [sym]: "value"
};

console.log(obj[sym]); // "value"

Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员。

const getClassNameSymbol = Symbol();

class C {
    [getClassNameSymbol](){
       return "C";
    }
}

let c = new C();
let className = c[getClassNameSymbol](); // "C"

众所周知的Symbols

除了用户定义的symbols,还有一些已经众所周知的内置symbols。 内置symbols用来表示语言内部的行为。

以下为这些symbols的列表:

Symbol.hasInstance

方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。

Symbol.isConcatSpreadable

布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。

Symbol.iterator

方法,被for-of语句调用。返回对象的默认迭代器。

Symbol.match

方法,被String.prototype.match调用。正则表达式用来匹配字符串。

Symbol.replace

方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。

Symbol.search

方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。

Symbol.species

函数值,为一个构造函数。用来创建派生对象。

Symbol.split

方法,被String.prototype.split调用。正则表达式来用分割字符串。

Symbol.toPrimitive

方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。

Symbol.toStringTag

方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。

Symbol.unscopables

对象,它自己拥有的属性会被with作用域排除在外。

三斜线指令

三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。

三斜线指令_仅_可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。

/// <reference path="..." />

/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的_依赖_。

三斜线引用告诉编译器在编译过程中要引入的额外的文件。

当使用--out--outFile时,它也可以做为调整输出内容顺序的一种方法。 文件在输出文件内容中的位置与经过预处理后的输入顺序一致。

预处理输入文件

编译器会对输入文件进行预处理来解析所有三斜线引用指令。 在这个过程中,额外的文件会加到编译过程中。

这个过程会以一些_根文件_开始; 它们是在命令行中指定的文件或是在tsconfig.json中的"files"列表里的文件。 这些根文件按指定的顺序进行预处理。 在一个文件被加入列表前,它包含的所有三斜线引用都要被处理,还有它们包含的目标。 三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析。

一个三斜线引用路径是相对于包含它的文件的,如果不是根文件。

错误

引用不存在的文件会报错。 一个文件用三斜线指令引用自己会报错。

使用 --noResolve

如果指定了--noResolve编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序。

/// <reference types="..." />

/// <reference path="..." />指令相似(用于声明_依赖_),/// <reference types="..." />指令声明了对某个包的依赖。

对这些包的名字的解析与在import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做import声明的包。

例如,把/// <reference types="node" />引入到声明文件,表明这个文件使用了@types/node/index.d.ts里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。

仅当在你需要写一个d.ts文件时才使用这个指令。

对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />; _当且仅当_结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。

若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里指定。 查看tsconfig.json里使用@typestypeRootstypes了解详情。

/// <reference no-default-lib="true"/>

这个指令把一个文件标记成_默认库_。 你会在lib.d.ts文件和它不同的变体的顶端看到这个注释。

这个指令告诉编译器在编译过程中_不要_包含这个默认库(比如,lib.d.ts)。 这与在命令行上使用--noLib相似。

还要注意,当传递了--skipDefaultLibCheck时,编译器只会忽略检查带有/// <reference no-default-lib="true"/>的文件。

/// <amd-module />

默认情况下生成的AMD模块都是匿名的。 但是,当一些工具需要处理生成的模块时会产生问题,比如r.js

amd-module指令允许给编译器传入一个可选的模块名:

amdModule.ts

///<amd-module name='NamedModule'/>
export class C {
}

这会将NamedModule传入到AMD define函数里:

amdModule.js

define("NamedModule", ["require", "exports"], function (require, exports) {
    var C = (function () {
        function C() {
        }
        return C;
    })();
    exports.C = C;
});

/// <amd-dependency />

注意:这个指令被废弃了。使用import "moduleName";语句代替。

/// <amd-dependency path="x" />告诉编译器有一个非TypeScript模块依赖需要被注入,做为目标模块require调用的一部分。

amd-dependency指令也可以带一个可选的name属性;它允许我们为amd-dependency传入一个可选名字:

/// <amd-dependency path="legacy/moduleA" name="moduleA"/>
declare var moduleA:MyType
moduleA.callStuff()

生成的JavaScript代码:

define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) {
    moduleA.callStuff()
});

类型兼容性

介绍

TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子:

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

在使用基于名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。

TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。

关于可靠性的注意事项

TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。

开始

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。比如:

interface Named {
    name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;

这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。 在这个例子中,y必须包含名字是namestring类型成员。y满足条件,因此赋值正确。

检查函数参数时使用相同的规则:

function greet(n: Named) {
    console.log('Hello, ' + n.name);
}
greet(y); // OK

注意,y有个额外的location属性,但这不会引发错误。 只有目标类型(这里是Named)的成员会被一一检查是否兼容。

这个比较过程是递归进行的,检查每个成员及子成员。

比较两个函数

相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。

第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。

你可能会疑惑为什么允许忽略参数,像例子y = x中那样。 原因是忽略额外的参数在JavaScript里是很常见的。 例如,Array#forEach给回调函数传3个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:

let items = [1, 2, 3];

// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));

下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error, because x() lacks a location property

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。例如:

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((e as MouseEvent).x + "," + (e as MouseEvent).y));
listenEvent(EventType.Mouse, ((e: MouseEvent) => console.log(e.x + "," + e.y)) as (e: Event) => void);

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

你可以使用strictFunctionTypes编译选项,使TypeScript在这种情况下报错。

可选参数及剩余参数

比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。

当一个函数有剩余参数时,它被当做无限个可选参数。

这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded

有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:

function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

函数重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  // Error

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK

类的私有成员和受保护成员

类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // OK, because y matches structure of x

上面代码里,xy是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // Error, because x and y are not compatible

在这里,泛型类型在使用时就好比不是一个泛型类型。

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。

比如,

let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // OK, because (x: any) => any matches (y: any) => any

高级主题

子类型与赋值

目前为止,我们使用了“兼容性”,它在语言规范里没有定义。 在TypeScript里,有两种兼容性:子类型和赋值。 它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any来回赋值,以及enum和对应数字值之间的来回赋值。

语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implementsextends语句也不例外。

更多信息,请参阅TypeScript语言规范.

类型推论

介绍

这节介绍TypeScript里的类型推论。即,类型是在哪里如何被推断的。

基础

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子

let x = 3;

变量x的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。

最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,

let x = [0, 1, null];

为了推断x的类型,我们必须考虑所有元素的类型。 这里有两种选择:numbernull。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:

let zoo = [new Rhino(), new Elephant(), new Snake()];

这里,我们想让zoo被推断为Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:

let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];

如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Rhino | Elephant | Snake)[]

上下文归类

TypeScript类型推论也可能按照相反的方向进行。 这被叫做“上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:

window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button);   //<- OK
    console.log(mouseEvent.kangaroo); //<- Error!
};

在这个例子里,TypeScript类型检查器会使用Window.onmousedown函数的类型来推断右边函数表达式的类型。 所以它能够推断出mouseEvent参数的类型中包含了button属性而不包含kangaroo属性。

TypeScript还能够很好地推断出其它上下文中的类型。

window.onscroll = function(uiEvent) {
    console.log(uiEvent.button); //<- Error!
}

上面的函数被赋值给window.onscrollTypeScript能够知道uiEventUIEvent,而不是MouseEventUIEvent对象不包含button属性,因此TypeScript会报错。

如果这个函数不是在上下文归类的位置上,那么这个函数的参数类型将隐式的成为any类型,而且也不会报错(除非你开启了--noImplicitAny选项):

const handler = function(uiEvent) {
    console.log(uiEvent.button); //<- OK
}

我们也可以明确地为函数参数类型赋值来覆写上下文类型:

window.onscroll = function(uiEvent: any) {
    console.log(uiEvent.button);  //<- Now, no error is given
};

但这段代码会打印undefined,因为uiEvent并不包含button属性。

上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:

function createZoo(): Animal[] {
    return [new Rhino(), new Elephant(), new Snake()];
}

这个例子里,最佳通用类型有4个候选者:AnimalRhinoElephantSnake。 当然,Animal会被做为最佳通用类型。

变量声明

变量声明

letconst是JavaScript里相对较新的变量声明方式。 像我们之前提到过的let在很多方面与var是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const是对let的一个增强,它能阻止对一个变量再次赋值。

因为TypeScript是JavaScript的超集,所以它本身就支持letconst。 下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替var

如果你之前使用JavaScript时没有特别在意,那么这节内容会唤起你的回忆。 如果你已经对var声明的怪异之处了如指掌,那么你可以轻松地略过这节。

var 声明

一直以来我们都是通过var关键字定义JavaScript变量。

var a = 10;

大家都能理解,这里定义了一个名为a值为10的变量。

我们也可以在函数内部定义变量:

function f() {
    var message = "Hello, world!";

    return message;
}

并且我们也可以在其它函数内部访问相同的变量。

function f() {
    var a = 10;
    return function g() {
        var b = a + 1;
        return b;
    }
}

var g = f();
g(); // returns 11;

上面的例子里,g可以获取到f函数里定义的a变量。 每当g被调用时,它都可以访问到f里的a变量。 即使当gf已经执行完后才被调用,它仍然可以访问及修改a

function f() {
    var a = 1;

    a = 2;
    var b = g();
    a = 3;

    return b;

    function g() {
        return a;
    }
}

f(); // returns 2

作用域规则

对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子:

function f(shouldInitialize: boolean) {
    if (shouldInitialize) {
        var x = 10;
    }

    return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'

有些读者可能要多看几遍这个例子。 变量x是定义在_if语句里面_,但是我们却可以在语句的外面访问它。 这是因为var声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为_var作用域_或_函数作用域_。 函数参数也使用函数作用域。

这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:

function sumMatrix(matrix: number[][]) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (var i = 0; i < currentRow.length; i++) {
            sum += currentRow[i];
        }
    }

    return sum;
}

这里很容易看出一些问题,里层的for循环会覆盖变量i,因为所有i都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。

捕获变量怪异之处

快速的猜一下下面的代码会返回什么:

for (var i = 0; i < 10; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}

介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。

好吧,看一下结果:

10
10
10
10
10
10
10
10
10
10

很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:

0
1
2
3
4
5
6
7
8
9

还记得我们上面提到的捕获变量吗? 我们传给setTimeout的每一个函数表达式实际上都引用了相同作用域里的同一个i

让我们花点时间思考一下这是为什么。 setTimeout在若干毫秒后执行一个函数,并且是在for循环结束后。 for循环结束后,i的值为10。 所以当函数被调用的时候,它会打印出10

一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:

for (var i = 0; i < 10; i++) {
    // capture the current state of 'i'
    // by invoking a function with its current value
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}

这种奇怪的形式我们已经司空见惯了。 参数i会覆盖for循环里的i,但是因为我们起了同样的名字,所以我们不用怎么改for循环体里的代码。

let 声明

现在你已经知道了var存在一些问题,这恰好说明了为什么用let语句来声明变量。 除了名字不同外,letvar的写法一致。

let hello = "Hello!";

主要的区别不在语法上,而是语义,我们接下来会深入研究。

块作用域

当用let声明一个变量,它使用的是_词法作用域_或_块作用域_。 不同于使用var声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。

function f(input: boolean) {
    let a = 100;

    if (input) {
        // Still okay to reference 'a'
        let b = a + 1;
        return b;
    }

    // Error: 'b' doesn't exist here
    return b;
}

这里我们定义了2个变量aba的作用域是f函数体内,而b的作用域是if语句块里。

catch语句里声明的变量也具有同样的作用域规则。

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);

拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于_暂时性死区_。 它只是用来说明我们不能在let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。

a++; // illegal to use 'a' before it's declared;
let a;

注意一点,我们仍然可以在一个拥有块作用域变量被声明前_获取_它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。

function foo() {
    // okay to capture 'a'
    return a;
}

// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();

let a;

关于_暂时性死区_的更多信息,查看这里Mozilla Developer Network.

重声明及屏蔽

我们提过使用var声明时,它不在乎你声明多少次;你只会得到1个。

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}

在上面的例子里,所有x的声明实际上都引用一个_相同_的x,并且这是完全有效的代码。 这经常会成为bug的来源。 好的是,let声明就不会这么宽松了。

let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`

并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。

function f(x) {
    let x = 100; // error: interferes with parameter declaration
}

function g() {
    let x = 100;
    var x = 100; // error: can't have both declarations of 'x'
}

并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

f(false, 0); // returns 0
f(true, 0);  // returns 100

在一个嵌套作用域里引入一个新名字的行为称做_屏蔽_。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用let重写之前的sumMatrix函数。

function sumMatrix(matrix: number[][]) {
    let sum = 0;
    for (let i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (let i = 0; i < currentRow.length; i++) {
            sum += currentRow[i];
        }
    }

    return sum;
}

这个版本的循环能得到正确的结果,因为内层循环的i可以屏蔽掉外层循环的i

_通常_来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。

块级作用域变量的获取

在我们最初谈及获取用var声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的_环境_。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}

因为我们已经在city的环境里获取到了city,所以就算if语句执行结束后我们仍然可以访问它。

回想一下前面setTimeout的例子,我们最后需要使用立即执行的函数表达式来获取每次for循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在TypeScript里这样做了。

let声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对_每次迭代_都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在setTimeout例子里我们仅使用let声明就可以了。

for (let i = 0; i < 10 ; i++) {
    setTimeout(function() {console.log(i); }, 100 * i);
}

会输出与预料一致的结果:

0
1
2
3
4
5
6
7
8
9

const 声明

const 声明是声明变量的另一种方式。

const numLivesForCat = 9;

它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与let相同的作用域规则,但是不能对它们重新赋值。

这很好理解,它们引用的值是_不可变的_。

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。

let vs. const

现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。

使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用const也可以让我们更容易的推测数据的流动。

跟据你的自己判断,如果合适的话,与团队成员商议一下。

这个手册大部分地方都使用了let声明。

解构

TypeScript 包含的另一个 ECMAScript 2015 特性就是解构。完整列表请参见 the article on the Mozilla Developer Network。 本章,我们将给出一个简短的概述。

解构数组

最简单的解构莫过于数组的解构赋值了:

let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

这创建了2个命名变量 firstsecond。 相当于使用了索引,但更为方便:

first = input[0];
second = input[1];

解构也可以作用于已声明的变量:

// swap variables
[first, second] = [second, first];

类似地,也可以作用于函数参数:

function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f([1, 2]);

你可以在数组里使用...语法创建剩余变量:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:

let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

或其它元素:

let [, second, , fourth] = [1, 2, 3, 4];
console.log(second); // outputs 2
console.log(fourth); // outputs 4

解构元组

元组可以像数组一样解构;解构后的变量获得对应元组元素的类型:

let tuple: [number, string, boolean] = [7, "hello", true];

let [a, b, c] = tuple; // a: number, b: string, c: boolean

当解构元组时,若超出元组索引范围将报错:

let [a, b, c, d] = tuple; // 错误,没有索引为3的元素

与数组一样,可以作用...来解构元组的剩余元素,从而得到一个短的元组:

let [a, ...bc] = tuple; // bc: [string, boolean]
let [a, b, c, ...d] = tuple; // d: [], the empty tuple

或者,忽略末尾元素或其它元素:

let [a] = tuple; // a: number
let [, b] = tuple; // b: string

对象解构

你也可以解构对象:

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;

这通过 o.a and o.b 创建了 ab 。 注意,如果你不需要 c 你可以忽略它。

就像数组解构,你可以用没有声明的赋值:

({ a, b } = { a: "baz", b: 101 });

注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。

你可以在对象里使用...语法创建剩余变量:

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

属性重命名

你也可以给属性以不同的名字:

let { a: newName1, b: newName2 } = o;

这里的语法开始变得混乱。 你可以将 a: newName1 读做 "a 作为 newName1"。 方向是从左到右,好像你写成了以下样子:

let newName1 = o.a;
let newName2 = o.b;

令人困惑的是,这里的冒号_不是_指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

let {a, b}: {a: string, b: number} = o;

默认值

我们可以为属性指定一个默认值,当属性值为undefined时,将使用该默认值:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}

此例中,b?表明b是可选的,因此它可能为undefined。 现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 ab 都会有值。

函数声明

解构也能用于函数声明。 看以下简单的情况:

type C = { a: string, b?: number }
function f({ a, b }: C): void {
    // ...
}

但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。

function f({ a="", b=0 } = {}): void {
    // ...
}
f();

上面的代码是一个类型推断的例子,将在本手册后文介绍。

其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:

function f({ a, b = 0 } = { a: "" }): void {
    // ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to {a: ""}, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument

要小心使用解构。 从前面的例子可以看出,就算是最简单的解构表达式也是难以理解的。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。

展开

展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

这会令bothPlus的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了firstsecond的一份浅拷贝。 它们不会被展开操作所改变。

你还可以展开对象:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

search的值为{ food: "rich", price: "$$", ambiance: "noisy" }。 对象的展开比数组的展开要复杂的多。 像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

那么,defaults里的food属性会重写food: "rich",在这里这并不是我们想要的结果。

对象展开还有其它一些意想不到的限制。 首先,它仅包含对象 自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法:

class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

其次,TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。

手册 v2

模版字面量类型

从 TypeScript 4.1 开始支持

模版字面量类型以字符串字面量类型为基础,且可以展开为多个字符串类型的联合类型。

其语法与 JavaScript 中的模版字面量是一致的,但是是用在类型的位置上。 当与某个具体的字面量类型一起使用时,模版字面量会将文本连接从而生成一个新的字符串字面量类型。

type World = 'world';

type Greeting = `hello ${World}`;
//   'hello world'

如果在替换字符串的位置是联合类型,那么结果类型是由每个联合类型成员构成的字符串字面量的集合:

type EmailLocaleIDs = 'welcome_email' | 'email_heading';
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff';

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

多个替换字符串的位置上的多个联合类型会进行交叉相乘:

type EmailLocaleIDs = 'welcome_email' | 'email_heading';
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff';

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = 'en' | 'ja' | 'pt';

type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
//   type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff';

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = 'en' | 'ja' | 'pt';

type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
//   "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"

我们还是建议开发者要提前生成数量巨大的字符串联合类型,但如果数量较少,那么上面介绍的方法会有所帮助。

类型中的字符串联合类型

模版字面量的强大之处在于它能够基于给定的字符串来创建新的字符串。

例如,JavaScript 中有一个常见的模式是基于对象的现有属性来扩展它。 下面我们定义一个函数类型on,它用于监听值的变化。

declare function makeWatchedObject(obj: any): any;

const person = makeWatchedObject({
    firstName: 'Saoirse',
    lastName: 'Ronan',
    age: 26,
});

person.on('firstNameChanged', (newValue) => {
    console.log(`firstName was changed to ${newValue}!`);
});

注意,on会监听"firstNameChanged"事件,而不是"firstName"。 模版字面量提供了操作字符串类型的能力:

type PropEventSource<Type> = {
    on(
        eventName: `${string & keyof Type}Changed`,
        callback: (newValue: any) => void
    ): void;
};

/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(
    obj: Type
): Type & PropEventSource<Type>;

这样做之后,当传入了错误的属性名会产生一个错误:

type PropEventSource<Type> = {
    on(
        eventName: `${string & keyof Type}Changed`,
        callback: (newValue: any) => void
    ): void;
};

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

const person = makeWatchedObject({
    firstName: 'Saoirse',
    lastName: 'Ronan',
    age: 26,
});

person.on('firstNameChanged', () => {});

// 以下存在拼写错误
person.on('firstName', () => {});
person.on('frstNameChanged', () => {});

模版字面量类型推断

注意,上例中没有使用原属性值的类型,在回调函数中仍使用any类型。 模版字面量类型能够从替换字符串的位置推断出类型。

下面,我们将上例修改成泛型,它会从eventName字符串来推断出属性名。

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>(
        eventName: `${Key}Changed`,
        callback: (newValue: Type[Key]) => void
    ): void;
};

declare function makeWatchedObject<Type>(
    obj: Type
): Type & PropEventSource<Type>;

const person = makeWatchedObject({
    firstName: 'Saoirse',
    lastName: 'Ronan',
    age: 26,
});

person.on('firstNameChanged', (newName) => {
    //                        string
    console.log(`new name is ${newName.toUpperCase()}`);
});

person.on('ageChanged', (newAge) => {
    //                  number
    if (newAge < 0) {
        console.warn('warning! negative age');
    }
});

这里,我们将on改为泛型方法。

当用户使用字符串"firstNameChanged'来调用时,TypeScript 会尝试推断K的类型。 为此,TypeScript 尝试将Key"Changed"之前的部分进行匹配,并且推断出字符串"firstName"。 当 TypeScript 推断出了类型后,on方法就能够获取firstName属性的类型,即string类型。 相似的,当使用"ageChanged"调用时,TypeScript 能够知道age属性的类型是number

类型推断可以以多种方式组合,例如拆解字符串然后以其它方式重新构造字符串。

操作固有字符串的类型

为了方便字符串操作,TypeScript 提供了一系列操作字符串的类型。 这些类型内置于编译器之中,以便提高性能。 它们不存在于 TypeScript 提供的.d.ts文件中。

Uppercase<StringType>

将字符串中的每个字符转换为大写字母。

Example
type Greeting = 'Hello, world';
type ShoutyGreeting = Uppercase<Greeting>;
//   "HELLO, WORLD"

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<'my_app'>;
//   "ID-MY_APP"

Lowercase<StringType>

将字符串中的每个字符转换为小写字母。

type Greeting = 'Hello, world';
type QuietGreeting = Lowercase<Greeting>;
//   "hello, world"

type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`;
type MainID = ASCIICacheKey<'MY_APP'>;
//   "id-my_app"

Capitalize<StringType>

将字符串中的首字母转换为大写字母。

Example
type LowercaseGreeting = 'hello, world';
type Greeting = Capitalize<LowercaseGreeting>;
//   "Hello, world"

Uncapitalize<StringType>

将字符串中的首字母转换为小写字母。

Example
type UppercaseGreeting = 'HELLO WORLD';
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
//   "hELLO WORLD"
固有字符串操作类型的技术细节

在TypeScript 4.1中会直接使用JavaScript中的字符串操作函数来操作固有字符串,且不会考虑本地化字符。

function applyStringMapping(symbol: Symbol, str: string) {
    switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
        case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
        case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
        case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
        case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
    }
    return str;
}

声明文件一章的目的是教你如何编写高质量的 TypeScript 声明文件。 我们假设你对 TypeScript 已经有了基本的了解。

如果没有,请先阅读TypeScript 手册 来了解一些基本知识,尤其是类型和模块的部分。

需要编写.d.ts文件的常见场景是为某个 npm 包添加类型信息。 如果是这种情况,你可以直接阅读Modules .d.ts

这篇指南被分成了以下章节。

示例

在编写声明文件时,我们经常遇到以下情况,那就是需要根据代码库提供的示例来编写声明文件。 示例一节展示了了许多常见的 API 模式,以及如何为它们编写声明文件。 该指南面向的是 TypeScript 的初学者,这些人可能并不熟悉 TypeScript 语言的每个特性。

结构

结构一节将帮助你了解常见库的格式以及如何为每种格式书写正确的声明文件。 如果你正在编辑一个已有文件,那么你可能不需要阅读此章节。 如果你在编写新的声明文件,那么强烈建议阅读此章节以理解库的不同格式是如何影响声明文件的编写的。

模版

模版一节里,你能找到一些声明文件,它们对于编写新的声明文件来讲会有所帮助。 如果你已经了解了库的结构,那么可以阅读相应的模版文件:

规范

声明文件里有些常见错误是很容易就可以避免的。 规范一节列出了常见的错误,并且描述了如何检测以及修复它们。 每个人都应该阅读这个章节以了解如何避免常见错误。

深入

针对那些对声明文件底层工作机制感兴趣的老手们,深入一节解释了编写声明文件时的很多高级概念, 并且展示了如何利用这些概念来创建整洁和直观的声明文件。

发布到 npm

发布一节讲解了如何将声明文件发布为 npm 包,以及如何管理包的依赖。

查找与安装声明文件

对于 JavaScript 库的使用者来讲,使用一节提供了一些简单步骤来查找与安装相应的声明文件。

举例

这篇指南的目的是教你如何书写高质量的 TypeScript 声明文件。 我们在这里会展示一些 API 的文档,以及它们的使用示例, 并且阐述了如何为它们书写声明文件。

这些例子是按复杂度递增的顺序组织的。

带属性的对象

文档

全局变量myLib包含一个用于创建祝福的makeGreeting函数, 以及表示祝福数量的numberOfGreetings属性。

代码

let result = myLib.makeGreeting('hello, world');
console.log('The computed greeting is:' + result);

let count = myLib.numberOfGreetings;

声明

使用declare namespace来描述用点表示法访问的类型或值。

declare namespace myLib {
    function makeGreeting(s: string): string;
    let numberOfGreetings: number;
}

函数重载

文档

getWidget函数接收一个数字参数并返回一个组件;或者接收一个字符串参数并返回一个组件数组。

代码

let x: Widget = getWidget(43);

let arr: Widget[] = getWidget('all of them');

声明

declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];

可重用类型(接口)

文档

当指定一个祝福词时,你必须传入一个GreetingSettings对象。 这个对象具有以下几个属性:

1- greeting:必需的字符串

2- duration: 可选的持续时间(以毫秒表示)

3- color: 可选的字符串,比如'#ff00ff'

代码

greet({
    greeting: 'hello world',
    duration: 4000,
});

声明

使用interface定义一个带有属性的类型。

interface GreetingSettings {
    greeting: string;
    duration?: number;
    color?: string;
}

declare function greet(setting: GreetingSettings): void;

可重用类型(类型别名)

文档

在任何需要祝福词的地方,你可以提供一个string,一个返回string的函数或一个Greeter实例。

代码

function getGreeting() {
    return 'howdy';
}
class MyGreeter extends Greeter {}

greet('hello');
greet(getGreeting);
greet(new MyGreeter());

声明

你可以使用类型别名来定义类型的短名:

type GreetingLike = string | (() => string) | MyGreeter;

declare function greet(g: GreetingLike): void;

组织类型

文档

greeter对象能够记录到文件或显示一个警告。 你可以为.log(...)提供 log 选项以及为.alert(...)提供 alert 选项。

代码

const g = new Greeter('Hello');
g.log({ verbose: true });
g.alert({ modal: false, title: 'Current Greeting' });

声明

使用命名空间组织类型。

declare namespace GreetingLib {
    interface LogOptions {
        verbose?: boolean;
    }
    interface AlertOptions {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

你也可以在一个声明中创建嵌套的命名空间:

declare namespace GreetingLib.Options {
    // Refer to via GreetingLib.Options.Log
    interface Log {
        verbose?: boolean;
    }
    interface Alert {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

文档

你可以通过实例化Greeter对象来创建祝福语,或者继承Greeter对象来自定义祝福语。

代码

const myGreeter = new Greeter('hello, world');
myGreeter.greeting = 'howdy';
myGreeter.showGreeting();

class SpecialGreeter extends Greeter {
    constructor() {
        super('Very special greetings');
    }
}

声明

使用declare class来描述一个类或像类一样的对象。 类可以有属性和方法,就和构造函数一样。

declare class Greeter {
    constructor(greeting: string);

    greeting: string;
    showGreeting(): void;
}

全局变量

文档

全局变量foo包含了存在的组件总数。

代码

console.log('Half the number of widgets is ' + foo / 2);

声明

使用declare var声明变量。 如果变量是只读的,那么可以使用declare const。 你还可以使用declare let,如果变量拥有块级作用域。

/** The number of widgets present */
declare var foo: number;

全局函数

文档

你可以使用一个字符串参数来调用greet函数,并向用户显示一条祝福语。

代码

greet('hello, world');

声明

使用declare function来声明函数。

declare function greet(greeting: string): void;

代码库结构

一般来讲,组织声明文件的方式取决于代码库是如何被使用的。 在 JavaScript 中一个代码库有很多使用方式,这就需要你书写声明文件去匹配它们。 这篇指南涵盖了如何识别常见代码库的模式,以及怎样书写符合相应模式的声明文件。

针对代码库的每种主要的组织模式,在模版一节都有对应的文件。 你可以利用它们帮助你快速上手。

识别代码库的类型

首先,我们先看一下 TypeScript 声明文件能够表示的库的类型。 这里会简单展示每种类型的代码库的使用方式,以及如何去书写,还有一些真实案例。

识别代码库的类型是书写声明文件的第一步。 我们将会给出一些提示,关于怎样通过代码库的使用方法及其源码来识别库的类型。 根据库的文档及组织结构的不同,在这两种方式中可能一个会比另外的一个简单一些。 我们推荐你使用任意你喜欢的方式。

你应该寻找什么?

在为代码库编写声明文件时,你需要问自己以下几个问题。

  1. 如何获取代码库?

    比如,是否只能够从 npm 或 CDN 获取。

  2. 如何导入代码库?

    它是否添加了某个全局对象?它是否使用了requireimport/export语句?

针对不同类型的代码库的示例

模块化代码库

几乎所有的 Node.js 代码库都属于这一类。 这类代码库只能工作在有模块加载器的环境下。 比如,express只能在 Node.js 里工作,所以必须使用 CommonJS 的require函数加载。

ECMAScript 2015(也就是 ES2015,ECMAScript 6 或 ES6),CommonJS 和 RequireJS 具有相似的导入一个模块的写法。 例如,对于 JavaScript CommonJS (Node.js),写法如下:

var fs = require('fs');

对于 TypeScript 或 ES6,import关键字也具有相同的作用:

import * as fs from 'fs';

你通常会在模块化代码库的文档里看到如下说明:

var someLib = require('someLib');

define(..., ['someLib'], function(someLib) {

});

与全局模块一样,你也可能会在 UMD 模块的文档里看到这些例子,因此要仔细查看源码和文档。

从代码上识别模块化代码库

模块化代码库至少会包含以下代表性条目之一:

  • 无条件的调用requiredefine
  • import * as a from 'b';export c;这样的声明
  • 赋值给exportsmodule.exports

它们极少包含:

  • windowglobal的赋值

模块化代码库的模版

有以下四个模版可用:

你应该先阅读module.d.ts以便从整体上了解它们的工作方式。

然后,若一个模块可以当作函数调用,则使用module-function.d.ts

const x = require('foo');
// Note: calling 'x' as a function
const y = x(42);

如果一个模块可以使用new来构造,则使用module-class.d.ts

var x = require('bar');
// Note: using 'new' operator on the imported variable
var y = new x('hello');

如果一个模块在导入后会更改其它的模块,则使用module-plugin.d.ts

const jest = require('jest');
require('jest-matchers-files');

全局代码库

全局代码库可以通过全局作用域来访问(例如,不使用任何形式的import语句)。 许多代码库只是简单地导出一个或多个供使用的全局变量。 比如,如果你使用jQuery,那么可以使用$变量来引用它。

$(() => {
    console.log('hello!');
});

你通常能够在文档里看到如何在 HTML 的 script 标签里引用代码库:

<script src="http://a.great.cdn.for/someLib.js"></script>

目前,大多数流行的全局代码库都以 UMD 代码库发布。 UMD 代码库与全局代码库很难通过文档来识别。 在编写全局代码库的声明文件之前,确保代码库不是 UMD 代码库。

从代码来识别全局代码库

通常,全局代码库的代码十分简单。 一个全局的“Hello, world”代码库可以如下:

function createGreeting(s) {
    return 'Hello, ' + s;
}

或者这样:

window.createGreeting = function (s) {
    return 'Hello, ' + s;
};

在阅读全局代码库的代码时,你会看到:

  • 顶层的var语句或function声明
  • 一个或多个window.someName赋值语句
  • 假设 DOM 相关的原始值documentwindow存在

你不会看到:

  • 检查或使用了模块加载器,如requiredefine
  • CommonJS/Node.js 风格的导入语句,如var fs = require("fs");
  • define(...)调用
  • 描述require或导入代码库的文档

全局代码库的示例

由于将全局代码库转换为 UMD 代码库十分容易,因此很少有代码库仍然使用全局代码库风格。 然而,小型的代码库以及需要使用 DOM 的代码库仍然可以是全局的。

全局代码库的模版

模版文件global.d.ts定义了myLib示例代码库。 请务必阅读脚注:"防止命名冲突"

UMD

一个 UMD 模块既可以用作 ES 模块(使用导入语句),也可以用作全局变量(在缺少模块加载器的环境中使用)。 许多流行的代码库,如Moment.js,都是使用这模式发布的。 例如,在 Node.js 中或使用了 RequireJS 时,你可以这样使用:

import moment = require('moment');
console.log(moment.format());

在纯浏览器环境中,你可以这样使用:

console.log(moment.format());

识别 UMD 代码库

UMD 模块会检查运行环境中是否存在模块加载器。 这是一种常见模式,示例如下:

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["libName"], factory);
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("libName"));
    } else {
        root.returnExports = factory(root.libName);
    }
}(this, function (b) {

如果你看到代码库中存在类如typeof definetypeof windowtypeof module的检测代码,尤其是在文件的顶端,那么它大概率是 UMD 代码库。

在 UMD 模块的文档中经常会提供在 Node.js 中结合require使用的示例,以及在浏览器中结合<script>标签使用的示例。

UMD 代码库的示例

大多数流行的代码库均提供了 UMD 格式的包。 例如,jQueryMoment.jslodash等。

模版

使用module-plugin.d.ts模版。

全局插件

一个全局插件是全局代码,它们会改变全局对象的结构。 对于全局修改的模块,在运行时存在冲突的可能。

比如,一些库往Array.prototypeString.prototype里添加新的方法。

识别全局插件

全局通常很容易地从它们的文档识别出来。

你会看到像下面这样的例子:

var x = 'hello, world';
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

模版

使用global-plugin.d.ts模版。

全局修改的模块

当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值。 比如,存在一些库它们添加新的成员到String.prototype当导入它们的时候。 这种模式很危险,因为可能造成运行时的冲突, 但是我们仍然可以为它们书写声明文件。

识别全局修改的模块

全局修改的模块通常可以很容易地从它们的文档识别出来。 通常来讲,它们与全局插件相似,但是需要require调用来激活它们的效果。

你可能会看到像下面这样的文档:

// 'require' call that doesn't use its return value
var unused = require('magic-string-time');
/* or */
require('magic-string-time');

var x = 'hello, world';
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

模版

使用global-modifying-module.d.ts模版。

利用依赖

你的代码库可能会有若干种依赖。 本节会介绍如何在声明文件中导入它们。

对全局库的依赖

如果你的代码库依赖于某个全局代码库,则使用/// <reference types="..." />指令:

/// <reference types="someLib" />

function getThing(): someLib.thing;

对模块的依赖

如果你的代码库依赖于某个模块,则使用import语句:

import * as moment from 'moment';

function getThing(): moment;

对 UMD 模块的依赖

全局代码库

如果你的全局代码库依赖于某个 UMD 模块,则使用/// <reference types指令:

/// <reference types="moment" />

function getThing(): moment;

ES 模块或 UMD 模块代码库

如果你的模块或 UMD 代码库依赖于某个 UMD 代码库,则使用import语句:

import * as someLib from 'someLib';

不要使用/// <reference指令来声明对 UMD 代码库的依赖。

脚注

防止命名冲突

注意,虽说可以在全局作用域内定义许多类型。 但我们强烈建议不要这样做,因为当一个工程中存在多个声明文件时,它可能会导致难以解决的命名冲突。

可以遵循的一个简单规则是使用代码库提供的某个全局变量来声明拥有命名空间的类型。 例如,如果代码库提供了全局变量cats,那么可以这样写:

declare namespace cats {
    interface KittySettings {}
}

而不是:

// at top-level
interface CatsKittySettings {}

这样做会保证代码库可以被转换成 UMD 模块,且不会影响声明文件的使用者。

ES6 对模块插件的影响

一些插件会对已有模块的顶层导出进行添加或修改。 这在 CommonJS 以及其它模块加载器里是合法的,但 ES6 模块是不可改变的,因此该模式是不可行的。 因为,TypeScript 是模块加载器无关的,所以在编译时不会对该行为加以限制,但是开发者若想要转换到 ES6 模块加载器则需要注意这一点。

ES6 对模块调用签名的影响

许多代码库,如 Express,将自身导出为可调用的函数。 例如,Express 的典型用法如下:

import exp = require('express');
var app = exp();

在 ES6 模块加载器中,顶层对象(此例中就exp)只能拥有属性; 顶层的模块对象永远不能够被调用。

最常见的解决方案是为可调用的/可构造的对象定义一个default导出; 有些模块加载器会自动检测这种情况并且将顶层对象替换为default导出。 如果在 tsconfig.json 里启用了"esModuleInterop": true,那么 Typescript 会自动为你处理。

模板

最佳实践

常规类型

NumberStringBooleanSymbolObject

不要使用以下类型NumberStringBooleanSymbolObject。 这些类型表示是非原始的封箱后的对象类型,它们几乎没有在 JavaScript 代码里被正确地使用过。

/* 错误 */
function reverse(s: String): String;

应该使用numberstringbooleansymbol类型。

/* 正确 */
function reverse(s: string): string;

使用非原始的object类型来代替Object类型(在 TypeScript 2.2 中新增

泛型

不要定义没有使用过类型参数的泛型类型。 更多详情请参考:TypeScript FAQ page

any

请尽量不要使用any类型,除非你正在将 JavaScript 代码迁移到 TypeScript 代码。 编译器实际上会将any视作“对其关闭类型检查”。 使用它与在每个变量前使用@ts-ignore注释是一样的。 它只在首次将 JavaScript 工程迁移到 TypeScript 工程时有用,因为你可以把还没有迁移完的实体标记为any类型,但在完整的 TypeScript 工程中,这样做就会禁用掉类型检查。

如果你不清楚要接收什么类型的数据,或者你希望接收任意类型并直接向下传递而不使用它,那么就可以使用unknown类型。

回调函数类型

回调函数的返回值类型

不要为返回值会被忽略的回调函数设置返回值类型any

/* 错误 */
function fn(x: () => any) {
    x();
}

应该为返回值会被忽略的回调函数设置返回值类型void

/* 正确 */
function fn(x: () => void) {
    x();
}

原因:使用void相对安全,因为它能防止不小心使用了未经检查的x的返回值:

function fn(x: () => void) {
    var k = x(); // oops! meant to do something else
    k.doSomething(); // error, but would be OK if the return type had been 'any'
}

回调函数里的可选参数

不要在回调函数里使用可选参数,除非这是你想要的:

/* 错误 */
interface Fetcher {
    getObject(done: (data: any, elapsedTime?: number) => void): void;
}

这里有具体的意义:done回调函数可以用 1 个参数或 2 个参数调用。 代码的大意是说该回调函数不关注是否有elapsedTime参数, 但是不需要把这个参数定义为可选参数来达到此目的 -- 因为总是允许提供一个接收较少参数的回调函数。

应该将回调函数定义为无可选参数:

/* 正确 */
interface Fetcher {
    getObject(done: (data: any, elapsedTime: number) => void): void;
}

重载与回调函数

不要因回调函数的参数数量不同而编写不同的重载。

/* WRONG */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(
    action: (done: DoneFn) => void,
    timeout?: number
): void;

应该只为最大数量参数的情况编写一个重载:

/* 正确 */
declare function beforeAll(
    action: (done: DoneFn) => void,
    timeout?: number
): void;

原因:回调函数总是允许忽略某个参数的,因此没必要为缺少可选参数的情况编写重载。 为缺少可选参数的情况提供重载可能会导致类型错误的回调函数被传入,因为它会匹配到第一个重载。

函数重载

顺序

不要把模糊的重载放在具体的重载前面:

/* 错误 */
declare function fn(x: any): any;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?

应该将重载排序,把具体的排在模糊的之前:

/* 正确 */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: any): any;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)

原因:当解析函数调用的时候,TypeScript 会选择匹配到的第一个重载。 当位于前面的重载比后面的“更模糊”,那么后面的会被隐藏且不会被选用。

使用可选参数

不要因为只有末尾参数不同而编写不同的重载:

/* WRONG */
interface Example {
    diff(one: string): number;
    diff(one: string, two: string): number;
    diff(one: string, two: string, three: boolean): number;
}

应该尽可能使用可选参数:

/* OK */
interface Example {
    diff(one: string, two?: string, three?: boolean): number;
}

注意,这只在返回值类型相同的情况是没问题的。

原因:有以下两个重要原因。

TypeScript 解析签名兼容性时会查看是否某个目标签名能够使用原参数调用, 且允许额外的参数。 下面的代码仅在签名被正确地使用可选参数定义时才会暴露出一个 bug:

function fn(x: (a: string, b: number, c: number) => void) {}
var x: Example;
// When written with overloads, OK -- used first overload
// When written with optionals, correctly an error
fn(x.diff);

第二个原因是当使用了 TypeScript “严格检查 null” 的特性时。 因为未指定的参数在 JavaScript 里表示为undefined,通常明确地为可选参数传入一个undefined不会有问题。 这段代码在严格 null 模式下可以工作:

var x: Example;
// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'
// When written with optionals, correctly OK
x.diff("something", true ? undefined : "hour");

使用联合类型

不要仅因某个特定位置上的参数类型不同而定义重载:

/* 错误 */
interface Moment {
  utcOffset(): number;
  utcOffset(b: number): Moment;
  utcOffset(b: string): Moment;
}

应该尽可能地使用联合类型:

/* 正确 */
interface Moment {
  utcOffset(): number;
  utcOffset(b: number | string): Moment;
}

注意,我们没有让b成为可选的,因为签名的返回值类型不同。

原因:这对于那些为该函数传入了值的使用者来说很重要。

function fn(x: string): void;
function fn(x: number): void;
function fn(x: number | string) {
  // When written with separate overloads, incorrectly an error
  // When written with union types, correctly OK
  return moment().utcOffset(x);
}

深入

组织模块以提供你想要的 API 结构是比较难的。 比如,你可能想要这样一个模块,可以用或不用new来创建不同的类型,在不同层级上暴露出不同的命名类型,且模块对象上还带有一些属性。

阅读这篇指南后,你就会了解如何编写复杂的声明文件来提供友好的 API 。 这篇指南针对于模块(或UMD)代码库,因为它们的选择具有更高的可变性。

核心概念

如果你理解了一些关于 TypeScript 是如何工作的核心概念, 那么你就能够为任何结构书写声明文件。

类型

如果你正在阅读这篇指南,你可能已经大概了解 TypeScript 里的类型指是什么。 明确一下,类型通过以下方式引入:

  • 类型别名声明(type sn = number | string;
  • 接口声明(interface I { x: number[]; }
  • 类声明(class C { }
  • 枚举声明(enum E { A, B, C }
  • 指向某个类型的import声明

以上每种声明形式都会创建一个新的类型名称。

与类型相比,你可能已经理解了什么是值。 值是运行时的名字,它可以在表达式里引用。 比如let x = 5;创建了一个名为x的值。

同样地,以下方式能够创建值:

  • letconst,和var声明
  • 包含值的namespacemodule声明
  • enum声明
  • class声明
  • 指向值的import声明
  • function声明

命名空间

类型可以存在于命名空间里。 比如,有这样的声明let x: A.B.C, 我们就认为C类型来自于A.B命名空间。

这个区别虽细微但很重要 -- 这里,A.B不是必需的类型或值。

简单的组合:一个名字,多种意义

一个给定的名字A,我们可以找出三种不同的意义:一个类型,一个值或一个命名空间。 要如何去解析这个名字要看它所在的上下文是怎样的。 比如,在声明let m: A.A = A;中,A首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!

这看上去让人迷惑,但是只要我们不过度的重载这还是很方便的。 下面让我们来看看一些有用的组合行为。

内置组合

眼尖的读者可能会注意到,比如,class同时出现在类型列表里。 class C { }声明创建了两个东西: 类型C指向类的实例结构, C指向类构造函数。 枚举声明拥有相似的行为。

用户定义组合

假设我们写了模块文件foo.d.ts:

export var SomeVar: { a: SomeType };
export interface SomeType {
  count: number;
}

这样使用它:

import * as foo from "./foo";
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

这可以很好地工作,但是我们知道SomeTypeSomeVar密切相关 因此我们想让它们有相同的名字。 我们可以使用组合通过相同的名字Bar表示这两种不同的对象(值和对象):

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}

这提供了使用解构的机会:

import { Bar } from "./foo";
let x: Bar = Bar.a;
console.log(x.count);

再次地,这里我们使用Bar做为类型和值。 注意我们没有声明Bar值为Bar类型 -- 它们是独立的。

高级组合

有一些声明能够通过多个声明组合。 比如,class C { }interface C { }可以同时存在并且都可以做为C类型的属性。

只要不产生冲突就是合法的。 一个普通的规则是值总是会和同名的其它值产生冲突,除非它们在不同命名空间里,类型冲突则发生在使用类型别名声明的情况下(type s = string),命名空间永远不会发生冲突。

让我们看看如何使用。

通过interface添加

我们可以使用一个interface向另一个interface声明里添加额外成员:

interface Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

这同样作用于类:

class Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

注意我们不能使用接口往类型别名里添加成员(type s = string;

通过namespace添加

namespace声明可以用来添加新类型,值和命名空间,只要不出现冲突即可。

比如,我们可以添加静态成员到一个类:

class C {}
// ... elsewhere ...
namespace C {
  export let x: number;
}
let y = C.x; // OK

注意在这个例子里,我们添加一个值到C静态部分(它的构造函数)。 这里因为我们添加了一个,且其它值的容器是另一个值(类型包含于命名空间,命名空间包含于另外的命名空间)。

我们还可以给类添加一个命名空间类型:

class C {}
// ... elsewhere ...
namespace C {
  export interface D {}
}
let y: C.D; // OK

在这个例子里,直到我们写了namespace声明才有了命名空间C。 做为命名空间的C不会与类创建的值C或类型C相互冲突。

最后,我们可以进行不同的合并通过namespace声明。

namespace X {
  export interface Y {}
  export class Z {}
}

// ... elsewhere ...
namespace X {
  export var Y: number;
  export namespace Z {
    export class C {}
  }
}
type X = string;

在这个例子里,第一个代码块创建了以下名字与含义:

  • 一个值X(因为namespace声明包含一个值,Z
  • 一个命名空间X(因为namespace声明包含一个类型,Y
  • 在命名空间X里的类型Y
  • 在命名空间X里的类型Z(类的实例结构)
  • X的一个属性值Z(类的构造函数)

第二个代码块创建了以下名字与含义:

  • Ynumber类型),它是值X的一个属性
  • 一个命名空间Z
  • Z,它是值X的一个属性
  • X.Z命名空间下的类型C
  • X.Z的一个属性值C
  • 类型X

使用export =import

一个重要的原则是exportimport声明会导出或导入目标的所有含义

发布

现在我们已经按照指南里的步骤写好了一个声明文件,是时候把它发布到 npm 了。 有两种主要方式用来将声明文件发布到 npm:

  1. 与你的 npm 包捆绑在一起,或
  2. 发布到 npm 上的@types organization

如果声明文件是由你写的源码生成的,那么就将声明文件与源码一起发布。 TypeScript 工程和 JavaScript 工程都可以使用--declaration选项来生成声明文件。

否则,我们推荐你将声明文件提交到 DefinitelyTyped,它会被发布到 npm 的@types里。

包含声明文件到你的 npm 包

如果你的包有一个主.js文件,你还需要在package.json里指定主声明文件。 设置types属性指向捆绑在一起的声明文件。 比如:

{
    "name": "awesome",
    "author": "Vandelay Industries",
    "version": "1.0.0",
    "main": "./lib/main.js",
    "types": "./lib/main.d.ts"
}

注意"typings""types"具有相同的意义,也可以使用它。

同样要注意的是如果主声明文件名是index.d.ts并且位置在包的根目录里(与index.js并列),你就不需要使用"types"属性指定了。

依赖

所有的依赖是由 npm 管理的。 确保所依赖的声明包都在package.json"dependencies"里指明了。 比如,假设我们写了一个包,它依赖于 Browserify 和 TypeScript。

{
    "name": "browserify-typescript-extension",
    "author": "Vandelay Industries",
    "version": "1.0.0",
    "main": "./lib/main.js",
    "types": "./lib/main.d.ts",
    "dependencies": {
        "browserify": "latest",
        "@types/browserify": "latest",
        "typescript": "next"
    }
}

这里,我们的包依赖于browserifytypescript包。 browserify没有把它的声明文件捆绑在它的 npm 包里,所以我们需要依赖于@types/browserify得到它的声明文件。 而typescript则相反,它把声明文件放在了 npm 包里,因此我们不需要依赖额外的包。

我们的包要从这两个包里暴露出声明文件,因此browserify-typescript-extension的用户也需要这些依赖。 正因此,我们使用"dependencies"而不是"devDependencies",否则用户将需要手动安装那些包。 如果我们只是在写一个命令行应用,并且我们的包不会被当做一个库使用的话,那么就可以使用devDependencies

危险信号

/// <reference path="..." />

不要在声明文件里使用/// <reference path="..." />

/// <reference path="../typescript/lib/typescriptServices.d.ts" />
....

应该使用/// <reference types="..." />代替

/// <reference types="typescript" />
....

务必阅读利用依赖一节了解详情。

打包所依赖的声明

如果你的类型声明依赖于另一个包:

  • 不要把依赖的包放进你的包里,保持它们在各自的文件里。
  • 不要将声明拷贝到你的包里。
  • 应该依赖在 npm 上的类型声明包,如果依赖包没包含它自己的声明文件的话。

使用typesVersions选择版本

当 TypeScript 打开一个package.json文件来决定要读取哪个文件,它首先会检查typesVersions字段。

带有typesVersions字段的package.json可能如下:

{
    "name": "package-name",
    "version": "1.0",
    "types": "./index.d.ts",
    "typesVersions": {
        ">=3.1": { "*": ["ts3.1/*"] }
    }
}

package.json告诉 TypeScript 去检查当前正在运行的 TypeScript 版本。 如果是 3.1 及以上版本,则会相对于package.json的位置来读取ts3.1目录的内容。 这就是{ "*": ["ts3.1/*"] }的含义 - 如果你熟悉路径映射的话,它们是相似的工作方式。

上例中,如果我们从"package-name"导入,当 TypeScript 版本为 3.1 时,TypeScript 会尝试解析[...]/node_modules/package-name/ts3.1/index.d.ts(及其它相应路径)。 如果导入的是package-name/foo,那么会尝试加载[...]/node_modules/package-name/ts3.1/foo.d.ts[...]/node_modules/package-name/ts3.1/foo/index.d.ts

那么如果不是在 TypeScript 3.1 环境中呢? 如果typesVersions中的每个字段都无法匹配,TypeScript 会回退到types字段,因此在 TypeScript 3.0 及之前的版本中会加载[...]/node_modules/package-name/index.d.ts文件。

匹配行为

TypeScript 是根据 Node.js 的语言化版本来进行编译器及语言版本匹配的。

存在多个字段

typesVersions支持同时指定多个字段,每个字段都指定了匹配的范围。

{
    "name": "package-name",
    "version": "1.0",
    "types": "./index.d.ts",
    "typesVersions": {
        ">=3.2": { "*": ["ts3.2/*"] },
        ">=3.1": { "*": ["ts3.1/*"] }
    }
}

由于指定的范围有发生重叠的潜在风险,因此声明文件的解析与指定的顺序是相关的。 也就是说,虽然>=3.2>=3.1都匹配 TypeScript 3.2 及以上版本,但调换顺序后会有不同的行为,因此上例不同于下例。

{
    "name": "package-name",
    "version": "1.0",
    "types": "./index.d.ts",
    "typesVersions": {
        // NOTE: this doesn't work!
        ">=3.1": { "*": ["ts3.1/*"] },
        ">=3.2": { "*": ["ts3.2/*"] }
    }
}

发布到@types

@types里的包是使用types-publisher 工具DefinitelyTyped里自动发布的。 如果想让你的包发布为@types包,提交一个 pull request 到https://github.com/DefinitelyTyped/DefinitelyTyped。 更多详情请参考contribution guidelines page

使用

下载

想要获取声明文件只需要用到 npm。

比如,想要获取 lodash 库的声明文件,只需使用下面的命令:

npm install --save @types/lodash

如果一个 npm 包像Publishing里介绍的一样已经包含其声明文件,那就不必再去下载相应的@types包了。

使用

下载完后,就可以直接在 TypeScript 里使用 lodash 了。 不论是在模块里还是全局代码里使用。

比如,你已经npm install安装了声明文件,你可以使用导入:

import * as _ from 'lodash';
_.padStart('Hello TypeScript!', 20, ' ');

或者如果你没有使用模块,那么你只需使用全局的变量_

_.padStart('Hello TypeScript!', 20, ' ');

查找

大多数情况下,类型声明包的名字总是与其在npm上的包的名字相同,但是有@types/前缀。 但如果你需要的话,你可以在https://aka.ms/types上查找你喜欢的库。

注意:如果你要找的声明文件不存在,你可以贡献一份,这样就方便了下一位开发者。 查看 DefinitelyTyped 贡献指南页了解详情。

JavaScript文件里的类型检查

TypeScript 2.3以后的版本支持使用--checkJs.js文件进行类型检查和错误提示。

你可以通过添加// @ts-nocheck注释来忽略类型检查;相反,你可以通过去掉--checkJs设置并添加一个// @ts-check注释来选择检查某些.js文件。 你还可以使用// @ts-ignore来忽略本行的错误。 如果你使用了tsconfig.json,JS检查将遵照一些严格检查标记,如noImplicitAnystrictNullChecks等。 但因为JS检查是相对宽松的,在使用严格标记时可能会有些出乎意料的情况。

对比.js文件和.ts文件在类型检查上的差异,有如下几点需要注意:

用JSDoc类型表示类型信息

.js文件里,类型可以和在.ts文件里一样被推断出来。 同样地,当类型不能被推断时,它们可以通过JSDoc来指定,就好比在.ts文件里那样。 如同TypeScript,--noImplicitAny会在编译器无法推断类型的位置报错。 (除了对象字面量的情况;后面会详细介绍)

JSDoc注解修饰的声明会被设置为这个声明的类型。比如:

/** @type {number} */
var x;

x = 0;      // OK
x = false;  // Error: boolean is not assignable to number

你可以在这里找到所有JSDoc支持的模式,JSDoc文档

属性的推断来自于类内的赋值语句

ES2015没提供声明类属性的方法。属性是动态赋值的,就像对象字面量一样。

.js文件里,编译器从类内部的属性赋值语句来推断属性类型。 属性的类型是在构造函数里赋的值的类型,除非它没在构造函数里定义或者在构造函数里是undefinednull。 若是这种情况,类型将会是所有赋的值的类型的联合类型。 在构造函数里定义的属性会被认为是一直存在的,然而那些在方法,存取器里定义的属性被当成可选的。

class C {
    constructor() {
        this.constructorOnly = 0
        this.constructorUnknown = undefined
    }
    method() {
        this.constructorOnly = false // error, constructorOnly is a number
        this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
        this.methodOnly = 'ok'  // ok, but methodOnly could also be undefined
    }
    method2() {
        this.methodOnly = true  // also, ok, methodOnly's type is string | boolean | undefined
    }
}

如果一个属性从没在类内设置过,它们会被当成未知的。

如果类的属性只是读取用的,那么就在构造函数里用JSDoc声明它的类型。 如果它稍后会被初始化,你甚至都不需要在构造函数里给它赋值:

class C {
    constructor() {
        /** @type {number | undefined} */
        this.prop = undefined;
        /** @type {number | undefined} */
        this.count;
    }
}

let c = new C();
c.prop = 0;          // OK
c.count = "string";  // Error: string is not assignable to number|undefined

构造函数等同于类

ES2015以前,Javascript使用构造函数代替类。 编译器支持这种模式并能够将构造函数识别为ES2015的类。 属性类型推断机制和上面介绍的一致。

function C() {
    this.constructorOnly = 0
    this.constructorUnknown = undefined
}
C.prototype.method = function() {
    this.constructorOnly = false // error
    this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}

支持CommonJS模块

.js文件里,TypeScript能识别出CommonJS模块。 对exportsmodule.exports的赋值被识别为导出声明。 相似地,require函数调用被识别为模块导入。例如:

// same as `import module "fs"`
const fs = require("fs");

// same as `export function readFile`
module.exports.readFile = function(f) {
  return fs.readFileSync(f);
}

对JavaScript文件里模块语法的支持比在TypeScript里宽泛多了。 大部分的赋值和声明方式都是允许的。

类,函数和对象字面量是命名空间

.js文件里的类是命名空间。 它可以用于嵌套类,比如:

class C {
}
C.D = class {
}

ES2015之前的代码,它可以用来模拟静态方法:

function Outer() {
  this.y = 2
}
Outer.Inner = function() {
  this.yy = 2
}

它还可以用于创建简单的命名空间:

var ns = {}
ns.C = class {
}
ns.func = function() {
}

同时还支持其它的变化:

// 立即调用的函数表达式
var ns = (function (n) {
  return n || {};
})();
ns.CONST = 1

// defaulting to global
var assign = assign || function() {
  // code goes here
}
assign.extra = 1

对象字面量是开放的

.ts文件里,用对象字面量初始化一个变量的同时也给它声明了类型。 新的成员不能再被添加到对象字面量中。 这个规则在.js文件里被放宽了;对象字面量具有开放的类型,允许添加并访问原先没有定义的属性。例如:

var obj = { a: 1 };
obj.b = 2;  // Allowed

对象字面量的表现就好比具有一个默认的索引签名[x:string]: any,它们可以被当成开放的映射而不是封闭的对象。

与其它JS检查行为相似,这种行为可以通过指定JSDoc类型来改变,例如:

/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2;  // Error, type {a: number} does not have property b

null,undefined,和空数组的类型是any或any[]

任何用nullundefined初始化的变量,参数或属性,它们的类型是any,就算是在严格null检查模式下。 任何用[]初始化的变量,参数或属性,它们的类型是any[],就算是在严格null检查模式下。 唯一的例外是像上面那样有多个初始化器的属性。

function Foo(i = null) {
    if (!i) i = 1;
    var j = undefined;
    j = 2;
    this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

函数参数是默认可选的

由于在ES2015之前无法指定可选参数,因此.js文件里所有函数参数都被当做是可选的。 使用比预期少的参数调用函数是允许的。

需要注意的一点是,使用过多的参数调用函数会得到一个错误。

例如:

function bar(a, b) {
  console.log(a + " " + b);
}

bar(1);       // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments

使用JSDoc注解的函数会被从这条规则里移除。 使用JSDoc可选参数语法来表示可选性。比如:

/**
 * @param {string} [somebody] - Somebody's name.
 */
function sayHello(somebody) {
    if (!somebody) {
        somebody = 'John Doe';
    }
    console.log('Hello ' + somebody);
}

sayHello();

arguments推断出的var-args参数声明

如果一个函数的函数体内有对arguments的引用,那么这个函数会隐式地被认为具有一个var-arg参数(比如:(...arg: any[]) => any))。使用JSDoc的var-arg语法来指定arguments的类型。

/** @param {...number} args */
function sum(/* numbers */) {
    var total = 0
    for (var i = 0; i < arguments.length; i++) {
      total += arguments[i]
    }
    return total
}

未指定的类型参数默认为any

由于JavaScript里没有一种自然的语法来指定泛型参数,因此未指定的参数类型默认为any

在extends语句中:

例如,React.Component被定义成具有两个类型参数,PropsState。 在一个.js文件里,没有一个合法的方式在extends语句里指定它们。默认地参数类型为any

import { Component } from "react";

class MyComponent extends Component {
    render() {
        this.props.b; // Allowed, since this.props is of type any
    }
}

使用JSDoc的@augments来明确地指定类型。例如:

import { Component } from "react";

/**
 * @augments {Component<{a: number}, State>}
 */
class MyComponent extends Component {
    render() {
        this.props.b; // Error: b does not exist on {a:number}
    }
}

在JSDoc引用中:

JSDoc里未指定的类型参数默认为any

/** @type{Array} */
var x = [];

x.push(1);        // OK
x.push("string"); // OK, x is of type Array<any>

/** @type{Array.<number>} */
var y = [];

y.push(1);        // OK
y.push("string"); // Error, string is not assignable to number

在函数调用中

泛型函数的调用使用arguments来推断泛型参数。有时候,这个流程不能够推断出类型,大多是因为缺少推断的源;在这种情况下,类型参数类型默认为any。例如:

var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

支持的JSDoc

下面的列表列出了当前所支持的JSDoc注解,你可以用它们在JavaScript文件里添加类型信息。

注意,没有在下面列出的标记(例如@async)都是还不支持的。

  • @type
  • @param (or @arg or @argument)
  • @returns (or @return)
  • @typedef
  • @callback
  • @template
  • @class (or @constructor)
  • @this
  • @extends (or @augments)
  • @enum

它们代表的意义与usejsdoc.org上面给出的通常是一致的或者是它的超集。 下面的代码描述了它们的区别并给出了一些示例。

@type

可以使用@type标记并引用一个类型名称(原始类型,TypeScript里声明的类型,或在JSDoc里@typedef标记指定的) 可以使用任何TypeScript类型和大多数JSDoc类型。

/**
 * @type {string}
 */
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

@type可以指定联合类型—例如,stringboolean类型的联合。

/**
 * @type {(string | boolean)}
 */
var sb;

注意,括号是可选的。

/**
 * @type {string | boolean}
 */
var sb;

有多种方式来指定数组类型:

/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

还可以指定对象字面量类型。 例如,一个带有a(字符串)和b(数字)属性的对象,使用下面的语法:

/** @type {{ a: string, b: number }} */
var var9;

可以使用字符串和数字索引签名来指定map-likearray-like的对象,使用标准的JSDoc语法或者TypeScript语法。

/**
 * A map-like object that maps arbitrary `string` properties to `number`s.
 *
 * @type {Object.<string, number>}
 */
var stringToNumber;

/** @type {Object.<number, object>} */
var arrayLike;

这两个类型与TypeScript里的{ [x: string]: number }{ [x: number]: any }是等同的。编译器能识别出这两种语法。

可以使用TypeScript或Closure语法指定函数类型。

/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

或者直接使用未指定的Function类型:

/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

Closure的其它类型也可以使用:

/**
 * @type {*} - can be 'any' type
 */
var star;
/**
 * @type {?} - unknown type (same as 'any')
 */
var question;

转换

TypeScript借鉴了Closure里的转换语法。 在括号表达式前面使用@type标记,可以将一种类型转换成另一种类型

/**
 * @type {number | string}
 */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

导入类型

可以使用导入类型从其它文件中导入声明。 这个语法是TypeScript特有的,与JSDoc标准不同:

/**
 * @param p { import("./a").Pet }
 */
function walk(p) {
    console.log(`Walking ${p.name}...`);
}

导入类型也可以使用在类型别名声明中:

/**
 * @typedef { import("./a").Pet } Pet
 */

/**
 * @type {Pet}
 */
var myPet;
myPet.name;

导入类型可以用在从模块中得到一个值的类型。

/**
 * @type {typeof import("./a").x }
 */
var x = require("./a").x;

@param@returns

@param语法和@type相同,但增加了一个参数名。 使用[]可以把参数声明为可选的:

// Parameters may be declared in a variety of syntactic forms
/**
 * @param {string}  p1 - A string param.
 * @param {string=} p2 - An optional param (Closure syntax)
 * @param {string} [p3] - Another optional param (JSDoc syntax).
 * @param {string} [p4="test"] - An optional param with a default value
 * @return {string} This is the result
 */
function stringsStringStrings(p1, p2, p3, p4){
  // TODO
}

函数的返回值类型也是类似的:

/**
 * @return {PromiseLike<string>}
 */
function ps(){}

/**
 * @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
 */
function ab(){}

@typedef, @callback, 和 @param

@typedef可以用来声明复杂类型。 和@param类似的语法。

/**
 * @typedef {Object} SpecialType - creates a new type named 'SpecialType'
 * @property {string} prop1 - a string property of SpecialType
 * @property {number} prop2 - a number property of SpecialType
 * @property {number=} prop3 - an optional number property of SpecialType
 * @prop {number} [prop4] - an optional number property of SpecialType
 * @prop {number} [prop5=42] - an optional number property of SpecialType with default
 */
/** @type {SpecialType} */
var specialTypeObject;

可以在第一行上使用objectObject

/**
 * @typedef {object} SpecialType1 - creates a new type named 'SpecialType1'
 * @property {string} prop1 - a string property of SpecialType1
 * @property {number} prop2 - a number property of SpecialType1
 * @property {number=} prop3 - an optional number property of SpecialType1
 */
/** @type {SpecialType1} */
var specialTypeObject1;

@param允许使用相似的语法。 注意,嵌套的属性名必须使用参数名做为前缀:

/**
 * @param {Object} options - The shape is the same as SpecialType above
 * @param {string} options.prop1
 * @param {number} options.prop2
 * @param {number=} options.prop3
 * @param {number} [options.prop4]
 * @param {number} [options.prop5=42]
 */
function special(options) {
  return (options.prop4 || 1001) + options.prop5;
}

@callback@typedef相似,但它指定函数类型而不是对象类型:

/**
 * @callback Predicate
 * @param {string} data
 * @param {number} [index]
 * @returns {boolean}
 */
/** @type {Predicate} */
const ok = s => !(s.length % 2);

当然,所有这些类型都可以使用TypeScript的语法@typedef在一行上声明:

/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

@template

使用@template声明泛型:

/**
 * @template T
 * @param {T} x - A generic parameter that flows through to the return type
 * @return {T}
 */
function id(x){ return x }

用逗号或多个标记来声明多个类型参数:

/**
 * @template T,U,V
 * @template W,X
 */

还可以在参数名前指定类型约束。 只有列表的第一项类型参数会被约束:

/**
 * @template {string} K - K must be a string or string literal
 * @template {{ serious(): string }} Seriousalizable - must have a serious method
 * @param {K} key
 * @param {Seriousalizable} object
 */
function seriousalize(key, object) {
  // ????
}

@constructor

编译器通过this属性的赋值来推断构造函数,但你可以让检查更严格提示更友好,你可以添加一个@constructor标记:

/**
 * @constructor
 * @param {number} data
 */
function C(data) {
  this.size = 0;
  this.initialize(data); // Should error, initializer expects a string
}
/**
 * @param {string} s
 */
C.prototype.initialize = function (s) {
  this.size = s.length
}

var c = new C(0);
var result = C(1); // C should only be called with new

通过@constructorthis将在构造函数C里被检查,因此你在initialize方法里得到一个提示,如果你传入一个数字你还将得到一个错误提示。如果你直接调用C而不是构造它,也会得到一个错误。

不幸的是,这意味着那些既能构造也能直接调用的构造函数不能使用@constructor

@this

编译器通常可以通过上下文来推断出this的类型。但你可以使用@this来明确指定它的类型:

/**
 * @this {HTMLElement}
 * @param {*} e
 */
function callbackForLater(e) {
    this.clientHeight = parseInt(e) // should be fine!
}

@extends

当JavaScript类继承了一个基类,无处指定类型参数的类型。而@extends标记提供了这样一种方式:

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

注意@extends只作用于类。当前,无法实现构造函数继承类的情况。

@enum

@enum标记允许你创建一个对象字面量,它的成员都有确定的类型。不同于JavaScript里大多数的对象字面量,它不允许添加额外成员。

/** @enum {number} */
const JSDocState = {
  BeginningOfLine: 0,
  SawAsterisk: 1,
  SavingComments: 2,
}

注意@enum与TypeScript的@enum大不相同,它更加简单。然而,不同于TypeScript的枚举,@enum可以是任何类型:

/** @enum {function(number): number} */
const Math = {
  add1: n => n + 1,
  id: n => -n,
  sub1: n => n - 1,
}

更多示例

var someObj = {
  /**
   * @param {string} param1 - Docs on property assignments work
   */
  x: function(param1){}
};

/**
 * As do docs on variable assignments
 * @return {Window}
 */
let someFunc = function(){};

/**
 * And class methods
 * @param {string} greeting The greeting to use
 */
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
 * And arrow functions expressions
 * @param {number} x - A multiplier
 */
let myArrow = x => x * x;

/**
 * Which means it works for stateless function components in JSX too
 * @param {{a: string, b: number}} test - Some param
 */
var fc = (test) => <div>{test.a.charAt(0)}</div>;

/**
 * A parameter can be a class constructor, using Closure syntax.
 *
 * @param {{new(...args: any[]): object}} C - The class to register
 */
function registerClass(C) {}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn10(p1){}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn9(p1) {
  return p1.join();
}

已知不支持的模式

在值空间中将对象视为类型是不可以的,除非对象创建了类型,如构造函数。

function aNormalFunction() {

}
/**
 * @type {aNormalFunction}
 */
var wrong;
/**
 * Use 'typeof' instead:
 * @type {typeof aNormalFunction}
 */
var right;

对象字面量属性上的=后缀不能指定这个属性是可选的:

/**
 * @type {{ a: string, b: number= }}
 */
var wrong;
/**
 * Use postfix question on the property name instead:
 * @type {{ a: string, b?: number }}
 */
var right;

Nullable类型只在启用了strictNullChecks检查时才启作用:

/**
 * @type {?number}
 * With strictNullChecks: true -- number | null
 * With strictNullChecks: off  -- number
 */
var nullable;

Non-nullable类型没有意义,以其原类型对待:

/**
 * @type {!number}
 * Just has type number
 */
var normal;

不同于JSDoc类型系统,TypeScript只允许将类型标记为包不包含null。 没有明确的Non-nullable -- 如果启用了strictNullChecks,那么number是非null的。 如果没有启用,那么number是可以为null的。

工程配置

tsconfig.json

概述

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。 tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:

使用tsconfig.json

  • 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。

当命令行上指定了输入文件时,tsconfig.json文件会被忽略。

示例

tsconfig.json示例文件:

  • 使用"files"属性
{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true
    },
    "files": [
        "core.ts",
        "sys.ts",
        "types.ts",
        "scanner.ts",
        "parser.ts",
        "utilities.ts",
        "binder.ts",
        "checker.ts",
        "emitter.ts",
        "program.ts",
        "commandLineParser.ts",
        "tsc.ts",
        "diagnosticInformationMap.generated.ts"
    ]
}
  • 使用"include""exclude"属性

    {
        "compilerOptions": {
            "module": "system",
            "noImplicitAny": true,
            "removeComments": true,
            "preserveConstEnums": true,
            "outFile": "../../built/local/tsc.js",
            "sourceMap": true
        },
        "include": [
            "src/**/*"
        ],
        "exclude": [
            "node_modules",
            "**/*.spec.ts"
        ]
    }
    

细节

"compilerOptions"可以被忽略,这时编译器会使用默认值。在这里查看完整的编译器选项列表。

"files"指定一个包含相对或绝对文件路径的列表。 "include""exclude"属性指定一个文件glob匹配模式列表。 支持的glob通配符有:

  • * 匹配0或多个字符(不包括目录分隔符)
  • ? 匹配一个任意字符(不包括目录分隔符)
  • **/ 递归匹配任意子目录

如果一个glob模式里的某部分只包含*.*,那么仅有支持的文件扩展名类型被包含在内(比如默认.ts.tsx,和.d.ts, 如果allowJs设置能true还包含.js.jsx)。

如果"files""include"都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts, .d.ts.tsx),排除在"exclude"里指定的文件。JS文件(.js.jsx)也被包含进来如果allowJs被设置成true。 如果指定了"files""include",编译器会将它们结合一并包含进来。 使用"outDir"指定的目录下的文件永远会被编译器排除,除非你明确地使用"files"将其包含进来(这时就算用exclude指定也没用)。

使用"include"引入的文件可以使用"exclude"属性过滤。 然而,通过"files"属性明确指定的文件却总是会被包含在内,不管"exclude"如何设置。 如果没有特殊指定,"exclude"默认情况下会排除node_modulesbower_componentsjspm_packages<outDir>目录。

任何被"files""include"指定的文件所引用的文件也会被包含进来。 A.ts引用了B.ts,因此B.ts不能被排除,除非引用它的A.ts"exclude"列表中。

需要注意编译器不会去引入那些可能做为输出的文件;比如,假设我们包含了index.ts,那么index.d.tsindex.js会被排除在外。 通常来讲,不推荐只有扩展名的不同来区分同目录下的文件。

tsconfig.json文件可以是个空文件,那么所有默认的文件(如上面所述)都会以默认配置选项编译。

在命令行上指定的编译选项会覆盖在tsconfig.json文件里的相应选项。

@typestypeRootstypes

默认所有_可见的_"@types"包会在编译过程中被包含进来。 node_modules/@types文件夹下以及它们子文件夹下的所有包都是_可见的_; 也就是说,./node_modules/@types/../node_modules/@types/../../node_modules/@types/等等。

如果指定了typeRoots只有typeRoots下面的包才会被包含进来。 比如:

{
   "compilerOptions": {
       "typeRoots" : ["./typings"]
   }
}

这个配置文件会包含_所有_./typings下面的包,而不包含./node_modules/@types里面的包。

如果指定了types,只有被列出来的包才会被包含进来。 比如:

{
   "compilerOptions": {
        "types" : ["node", "lodash", "express"]
   }
}

这个tsconfig.json文件将_仅会_包含 ./node_modules/@types/node./node_modules/@types/lodash./node_modules/@types/express。/@types/。 node_modules/@types/*里面的其它包不会被引入进来。

指定"types": []来禁用自动引入@types包。

注意,自动引入只在你使用了全局的声明(相反于模块)时是重要的。 如果你使用import "foo"语句,TypeScript仍然会查找node_modulesnode_modules/@types文件夹来获取foo包。

使用extends继承配置

tsconfig.json文件可以利用extends属性从另一个配置文件里继承配置。

extendstsconfig.json文件里的顶级属性(与compilerOptionsfilesinclude,和exclude一样)。 extends的值是一个字符串,包含指向另一个要继承文件的路径。

在原文件里的配置先被加载,然后被来自继承文件里的配置重写。 如果发现循环引用,则会报错。

来自所继承配置文件的filesincludeexclude_覆盖_源配置文件的属性。

配置文件里的相对路径在解析时相对于它所在的文件。

比如:

configs/base.json

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

tsconfig.json

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

tsconfig.nostrictnull.json

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

compileOnSave

在最顶层设置compileOnSave标记,可以让IDE在保存文件的时候根据tsconfig.json重新生成文件。

{
    "compileOnSave": true,
    "compilerOptions": {
        "noImplicitAny" : true
    }
}

要想支持这个特性需要Visual Studio 2015, TypeScript1.8.4以上并且安装atom-typescript插件。

模式

到这里查看模式: http://json.schemastore.org/tsconfig.

工程引用

工程引用是TypeScript 3.0的新特性,它支持将TypeScript程序的结构分割成更小的组成部分。

这样可以改善构建时间,强制在逻辑上对组件进行分离,更好地组织你的代码。

TypeScript 3.0还引入了tsc的一种新模式,即--build标记,它与工程引用协同工作可以加速TypeScript的构建。

一个工程示例

让我们来看一个非常普通的工程,并瞧瞧工程引用特性是如何帮助我们更好地组织代码的。 假设这个工程具有两个模块:converterunites,以及相应的测试代码:

/src/converter.ts
/src/units.ts
/test/converter-tests.ts
/test/units-tests.ts
/tsconfig.json

测试文件导入相应的实现文件并进行测试:

// converter-tests.ts
import * as converter from "../converter";

assert.areEqual(converter.celsiusToFahrenheit(0), 32);

在之前,这种使用单一tsconfig文件的结构会稍显笨拙:

  • 实现文件也可以导入测试文件
  • 无法同时构建testsrc,除非把src也放在输出文件夹中,但通常并不想这样做
  • 仅对实现文件的_内部_细节进行改动,必需再次对测试进行_类型检查_,尽管这是根本不必要的
  • 仅对测试文件进行改动,必需再次对实现文件进行_类型检查_,尽管其实什么都没有变

你可以使用多个tsconfig文件来解决_部分_问题,但是又会出现新问题:

  • 缺少内置的实时检查,因此你得多次运行tsc
  • 多次调用tsc会增加我们等待的时间
  • tsc -w不能一次在多个配置文件上运行

工程引用可以解决全部这些问题,而且还不止。

何为工程引用?

tsconfig.json增加了一个新的顶层属性references。它是一个对象的数组,指明要引用的工程:

{
    "compilerOptions": {
        // The usual
    },
    "references": [
        { "path": "../src" }
    ]
}

每个引用的path属性都可以指向到包含tsconfig.json文件的目录,或者直接指向到配置文件本身(名字是任意的)。

当你引用一个工程时,会发生下面的事:

  • 导入引用工程中的模块实际加载的是它_输出_的声明文件(.d.ts)。
  • 如果引用的工程生成一个outFile,那么这个输出文件的.d.ts文件里的声明对于当前工程是可见的。
  • 构建模式(后文)会根据需要自动地构建引用的工程。

当你拆分成多个工程后,会显著地加速类型检查和编译,减少编辑器的内存占用,还会改善程序在逻辑上进行分组。

composite

引用的工程必须启用新的composite设置。 这个选项用于帮助TypeScript快速确定引用工程的输出文件位置。 若启用composite标记则会发生如下变动:

  • 对于rootDir设置,如果没有被显式指定,默认为包含tsconfig文件的目录
  • 所有的实现文件必须匹配到某个include模式或在files数组里列出。如果违反了这个限制,tsc会提示你哪些文件未指定。
  • 必须开启declaration选项。

declarationMaps

我们增加了对declaration source maps的支持。 如果启用--declarationMap,在某些编辑器上,你可以使用诸如“Go to Definition”,重命名以及跨工程编辑文件等编辑器特性。

prependoutFile

你可以在引用中使用prepend选项来启用前置某个依赖的输出:

   "references": [
       { "path": "../utils", "prepend": true }
   ]

前置工程会将工程的输出添加到当前工程的输出之前。 它对.js文件和.d.ts文件都有效,source map文件也同样会正确地生成。

tsc永远只会使用磁盘上已经存在的文件来进行这个操作,因此你可能会创建出一个无法生成正确输出文件的工程,因为有些工程的输出可能会在结果文件中重覆了多次。 例如:

   A
  ^ ^
 /   \
B     C
 ^   ^
  \ /
   D

这种情况下,不能前置引用,因为在D的最终输出里会有两份A存在 - 这可能会发生未知错误。

关于工程引用的说明

工程引用在某些方面需要你进行权衡.

因为有依赖的工程要使用它的依赖生成的.d.ts,因此你必须要检查相应构建后的输出_或_在下载源码后进行构建,然后才能在编辑器里自由地导航。 我们是在操控幕后的.d.ts生成过程,我们应该减少这种情况,但是目前还们建议提示开发者在下载源码后进行构建。

此外,为了兼容已有的构建流程,tsc_不会_自动地构建依赖项,除非启用了--build选项。 下面让我们看看--build

TypeScript构建模式

在TypeScript工程里支持增量构建是个期待已久的功能。 在TypeScrpt 3.0里,你可以在tsc上使用--build标记。 它实际上是个新的tsc入口点,它更像是一个构建的协调员而不是简简单单的编译器。

运行tsc --build(简写tsc -b)会执行如下操作:

  • 找到所有引用的工程
  • 检查它们是否为最新版本
  • 按顺序构建非最新版本的工程

可以给tsc -b指定多个配置文件地址(例如:tsc -b src test)。 如同tsc -p,如果配置文件名为tsconfig.json,那么文件名则可省略。

tsc -b命令行

你可以指令任意数量的配置文件:

 > tsc -b                                # Run the tsconfig.json in the current directory
 > tsc -b src                            # Run src/tsconfig.json
 > tsc -b foo/prd.tsconfig.json bar  # Run foo/prd.tsconfig.json and bar/tsconfig.json

不需要担心命令行上指定的文件顺序 - tsc会根据需要重新进行排序,被依赖的项会优先构建。

tsc -b还支持其它一些选项:

  • --verbose:打印详细的日志(可以与其它标记一起使用)
  • --dry: 显示将要执行的操作但是并不真正进行这些操作
  • --clean: 删除指定工程的输出(可以与--dry一起使用)
  • --force: 把所有工程当作非最新版本对待
  • --watch: 观察模式(可以与--verbose一起使用)

说明

一般情况下,就算代码里有语法或类型错误,tsc也会生成输出(.js.d.ts),除非你启用了noEmitOnError选项。 这在增量构建系统里就不好了 - 如果某个过期的依赖里有一个新的错误,那么你只能看到它_一次_,因为后续的构建会跳过这个最新的工程。 正是这个原因,tsc -b的作用就好比在所有工程上启用了noEmitOnError

如果你想要提交所有的构建输出(.js, .d.ts, .d.ts.map等),你可能需要运行--force来构建,因为一些源码版本管理操作依赖于源码版本管理工具保存的本地拷贝和远程拷贝的时间戳。

MSBuild

如果你的工程使用msbuild,你可以用下面的方式开启构建模式。

    <TypeScriptBuildMode>true</TypeScriptBuildMode>

将这段代码添加到proj文件。它会自动地启用增量构建模式和清理工作。

注意,在使用tsconfig.json / -p时,已存在的TypeScript工程属性会被忽略 - 因此所有的设置需要在tsconfig文件里进行。

一些团队已经设置好了基于msbuild的构建流程,并且tsconfig文件具有和它们匹配的工程一致的_隐式_图序。 若你的项目如此,那么可以继续使用msbuildtsc -p以及工程引用;它们是完全互通的。

指导

整体结构

tsconfig.json多了以后,通常会使用配置文件继承来集中管理公共的编译选项。 这样你就可以在一个文件里更改配置而不必在多个文件中进行修改。

另一个最佳实践是有一个solution级别的tsconfig.json文件,它仅仅用于引用所有的子工程。 它用于提供一个简单的入口;比如,在TypeScript源码里,我们可以简单地运行tsc -b src来构建所有的节点,因为我们在src/tsconfig.json文件里列出了所有的子工程。 注意从3.0开始,如果tsconfig.json文件里有至少一个工程引用reference,那么files数组为空的话也不会报错。

你可以在TypeScript源码仓库里看到这些模式 - 阅读src/tsconfig_base.jsonsrc/tsconfig.jsonsrc/tsc/tsconfig.json

相对模块的结构

通常地,将代码转成使用相对模块并不需要改动太多。 只需在某个给定父目录的每个子目录里放一个tsconfig.json文件,并相应添加reference。 然后将outDir指定为输出目录的子目录或将rootDir指定为所有工程的某个公共根目录。

outFile的结构

使用了outFile的编译输出结构十分灵活,因为相对路径是无关紧要的。 要注意的是,你通常不需要使用prepend - 因为这会改善构建时间并结省I/O。 TypeScript项目本身是一个好的参照 - 我们有一些“library”的工程和一些“endpoint”工程,“endpoint”工程会确保足够小并仅仅导入它们需要的“library”。

NPM 包的类型

编译选项

编译选项

选项类型默认值描述
--allowJsbooleanfalse允许编译javascript文件。
--allowSyntheticDefaultImportsbooleanmodule === "system"或设置了--esModuleInterop允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
--allowUnreachableCodebooleanfalse不报告执行不到的代码错误。
--allowUnusedLabelsbooleanfalse不报告未使用的标签错误。
--alwaysStrictbooleanfalse以严格模式解析并为每个源文件生成"use strict"语句
--baseUrlstring解析非相对模块名的基准目录。查看模块解析文档了解详情。
--build -bbooleanfalse使用Project References来构建此工程及其依赖工程。注意这个标记与本页内其它标记不兼容。详情参考这里
--charsetstring"utf8"输入文件的字符集。
--checkJsbooleanfalse在.js文件中报告错误。与--allowJs配合使用。
--compositebooleantrue确保TypeScript能够找到编译当前工程所需要的引用工程的输出位置。
--declaration -dbooleanfalse生成相应的.d.ts文件。
--declarationDirstring生成声明文件的输出路径。
--diagnosticsbooleanfalse显示诊断信息。
--disableSizeLimitbooleanfalse禁用JavaScript工程体积大小的限制
--emitBOMbooleanfalse在输出文件的开头加入BOM头(UTF-8 Byte Order Mark)。
--emitDecoratorMetadata[1]booleanfalse给源码里的装饰器声明加上设计类型元数据。查看issue #2577了解更多信息。
--experimentalDecorators[1]booleanfalse启用实验性的ES装饰器。
--extendedDiagnosticsbooleanfalse显示详细的诊段信息。
--forceConsistentCasingInFileNamesbooleanfalse禁止对同一个文件的不一致的引用。
--generateCpuProfilestringprofile.cpuprofile在指定目录生成CPU资源使用报告。若传入的是已创建的目录名,将在此目录下生成以时间戳命名的报告。
--help -h打印帮助信息。
--importHelpersstringtslib导入辅助工具函数(比如__extends__rest等)
--importsNotUsedAsValuesstringremove用于设置针对于类型导入的代码生成和代码检查的行为。"remove""preserve"设置了是否对未使用的导入了模块副作用的导入语句生成相关代码,"error"则强制要求只用作类型的模块导入必须使用import type语句。
--inlineSourceMapbooleanfalse生成单个sourcemaps文件,而不是将每sourcemaps生成不同的文件。
--inlineSourcesbooleanfalse将代码与sourcemaps生成到一个文件中,要求同时设置了--inlineSourceMap--sourceMap属性。
--init初始化TypeScript项目并创建一个tsconfig.json文件。
--isolatedModulesbooleanfalse执行额外检查以确保单独编译(如transpileModule@babel/plugin-transform-typescript)是安全的。
--jsxstring"preserve".tsx文件里支持JSX:"react""preserve""react-native"。查看JSX
--jsxFactorystring"React.createElement"指定生成目标为react JSX时,使用的JSX工厂函数,比如React.createElementh
--libstring[]编译过程中需要引入的库文件的列表。 可能的值为: ► ES5ES6ES2015ES7ES2016ES2017ES2018ESNextDOMDOM.IterableWebWorkerScriptHostES2015.CoreES2015.CollectionES2015.GeneratorES2015.IterableES2015.PromiseES2015.ProxyES2015.ReflectES2015.SymbolES2015.Symbol.WellKnownES2016.Array.IncludeES2017.objectES2017.IntlES2017.SharedMemoryES2017.StringES2017.TypedArraysES2018.IntlES2018.PromiseES2018.RegExpESNext.AsyncIterableESNext.ArrayESNext.IntlESNext.Symbol 注意:如果--lib没有指定默认注入的库的列表。默认注入的库为: ► 针对于--target ES5DOM,ES5,ScriptHost ► 针对于--target ES6DOM,ES6,DOM.Iterable,ScriptHost
--listEmittedFilesbooleanfalse打印出编译后生成文件的名字。
--listFilesbooleanfalse编译过程中打印文件名。
--localestring(platform specific)显示错误信息时使用的语言,比如:en-us。
--mapRootstring为调试器指定指定sourcemap文件的路径,而不是使用生成时的路径。当.map文件是在运行时指定的,并不同于js文件的地址时使用这个标记。指定的路径会嵌入到sourceMap里告诉调试器到哪里去找它们。使用此标识并不会新创建指定目录并生成map文件在指定路径下。而是增加一个构建后的步骤,把相应文件移动到指定路径下。
--maxNodeModuleJsDepthnumber0node_modules依赖的最大搜索深度并加载JavaScript文件。仅适用于--allowJs
--module -mstringtarget === "ES6" ? "ES6" : "commonjs"指定生成哪个模块系统代码:"None""CommonJS""AMD""System""UMD""ES6""ES2015"。 ► 只有"AMD""System"能和--outFile一起使用。 ►"ES6""ES2015"可使用在目标输出为"ES5"或更低的情况下。
--moduleResolutionstringmodule === "AMD" or "System" or "ES6" ? "Classic" : "Node"决定如何处理模块。或者是"Node"对于Node.js/io.js,或者是"Classic"(默认)。查看模块解析了解详情。
--newLinestring(platform specific)当生成文件时指定行结束符:"crlf"(windows)或"lf"(unix)。
--noEmitbooleanfalse不生成输出文件。
--noEmitHelpersbooleanfalse不在输出文件中生成用户自定义的帮助函数代码,如__extends
--noEmitOnErrorbooleanfalse报错时不生成输出文件。
--noErrorTruncationbooleanfalse不截短错误消息。
--noFallthroughCasesInSwitchbooleanfalse报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿)
--noImplicitAnybooleanfalse在表达式和声明上有隐含的any类型时报错。
--noImplicitReturnsbooleanfalse不是函数的所有返回路径都有返回值时报错。
--noImplicitThisbooleanfalsethis表达式的值为any类型的时候,生成一个错误。
--noImplicitUseStrictbooleanfalse模块输出中不包含"use strict"指令。
--noLibbooleanfalse不包含默认的库文件(lib.d.ts)。
--noResolvebooleanfalse不把/// <reference``>或模块导入的文件加到编译文件列表。
--noStrictGenericChecksbooleanfalse禁用在函数类型里对泛型签名进行严格检查。
--noUnusedLocalsbooleanfalse若有未使用的局部变量则抛错。
--noUnusedParametersbooleanfalse若有未使用的参数则抛错。
--outstring弃用。使用 --outFile 代替。
--outDirstring重定向输出目录。
--outFilestring将输出文件合并为一个文件。合并的顺序是根据传入编译器的文件顺序和///<reference``>import的文件顺序决定的。查看输出文件顺序文档了解详情
paths[2]Object模块名到基于baseUrl的路径映射的列表。查看模块解析文档了解详情。
--preserveConstEnumsbooleanfalse保留constenum声明。查看const enums documentation了解详情。
--preserveSymlinksbooleanfalse不把符号链接解析为其真实路径;将符号链接文件视为真正的文件。
--preserveWatchOutputbooleanfalse保留watch模式下过时的控制台输出。
--pretty[1]booleanfalse给错误和消息设置样式,使用颜色和上下文。
--project -pstring编译指定目录下的项目。这个目录应该包含一个tsconfig.json文件来管理编译。查看tsconfig.json文档了解更多信息。
--reactNamespacestring"React"当目标为生成"react" JSX时,指定createElement__spread的调用对象
--removeCommentsbooleanfalse删除所有注释,除了以/!*开头的版权信息。
--rootDirstring(common root directory is computed from the list of input files)仅用来控制输出的目录结构--outDir
rootDirs[2]string[]根(root)文件夹列表,表示运行时组合工程结构的内容。查看模块解析文档了解详情。
--showConfigbooleanfalse不真正执行build,而是显示build使用的配置文件信息。
--skipDefaultLibCheckbooleanfalse忽略库的默认声明文件的类型检查。
--skipLibCheckbooleanfalse忽略所有的声明文件(*.d.ts)的类型检查。
--sourceMapbooleanfalse生成相应的.map文件。
--sourceRootstring指定TypeScript源文件的路径,以便调试器定位。当TypeScript文件的位置是在运行时指定时使用此标记。路径信息会被加到sourceMap里。
--strictbooleanfalse启用所有严格检查选项。 包含--noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes--strictPropertyInitialization.
--strictFunctionTypesbooleanfalse禁用函数参数双向协变检查。
--strictPropertyInitializationbooleanfalse确保类的非undefined属性已经在构造函数里初始化。若要令此选项生效,需要同时启用--strictNullChecks
--strictNullChecksbooleanfalse在严格的null检查模式下,nullundefined值不包含在任何类型里,只允许用它们自己和any来赋值(有个例外,undefined可以赋值到void)。
--suppressExcessPropertyErrors[1]booleanfalse阻止对对象字面量的额外属性检查。
--suppressImplicitAnyIndexErrorsbooleanfalse阻止--noImplicitAny对缺少索引签名的索引对象报错。查看issue #1232了解详情。
--target -tstring"ES3"指定ECMAScript目标版本"ES3"(默认),"ES5""ES6"/"ES2015""ES2016""ES2017""ES2018""ES2019""ES2020""ESNext"。 注意:"ESNext"最新的生成目标列表为ES proposed features
--traceResolutionbooleanfalse生成模块解析日志信息
--typesstring[]要包含的类型声明文件名列表。查看@types,--typeRoots和--types章节了解详细信息。
--typeRootsstring[]要包含的类型声明文件路径列表。查看@types,--typeRoots和--types章节了解详细信息。
--version -v打印编译器版本号。
--watch -w在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。监视文件和目录的具体实现可以通过环境变量进行配置。详情请看配置 Watch
  • [1] 这些选项是试验性的。
  • [2] 这些选项只能在tsconfig.json里使用,不能在命令行使用。

相关信息

配置 Watch

编译器支持使用环境变量配置如何监视文件和目录的变化。

使用TSC_WATCHFILE环境变量来配置文件监视

选项描述
PriorityPollingInterval使用fs.watchFile但针对源码文件,配置文件和消失的文件使用不同的轮询间隔
DynamicPriorityPolling使用动态队列,对经常被修改的文件使用较短的轮询间隔,对未修改的文件使用较长的轮询间隔
UseFsEvents使用 fs.watch,它使用文件系统事件(但在不同的系统上可能不一定准确)来查询文件的修改/创建/删除。注意少数的系统如Linux,对监视者的数量有限制,如果使用fs.watch创建监视失败那么将通过fs.watchFile来创建监视
UseFsEventsWithFallbackDynamicPolling此选项与UseFsEvents类似,只不过当使用fs.watch创建监视失败后,回退到使用动态轮询队列进行监视(如DynamicPriorityPolling介绍的那样)
UseFsEventsOnParentDirectory此选项通过fs.watch(使用系统文件事件)监视文件的父目录,因此CPU占用率低但也会降低精度
默认 (无指定值)如果环境变量TSC_NONPOLLING_WATCHER设置为true,监视文件的父目录(如同UseFsEventsOnParentDirectory)。否则,使用fs.watchFile监视文件,超时时间为250ms

使用TSC_WATCHDIRECTORY环境变量来配置目录监视

在那些Nodejs原生就不支持递归监视目录的平台上,我们会根据TSC_WATCHDIRECTORY的不同选项递归地创建对子目录的监视。 注意在那些原生就支持递归监视目录的平台上(如Windows),这个环境变量会被忽略。

选项描述
RecursiveDirectoryUsingFsWatchFile使用fs.watchFile监视目录和子目录,它是一个轮询监视(消耗CPU周期)
RecursiveDirectoryUsingDynamicPriorityPolling使用动态轮询队列来获取目录与其子目录的改变
默认 (无指定值)使用fs.watch来监视目录及其子目录

背景

在编译器中--watch的实现依赖于Nodejs提供的fs.watchfs.watchFile,两者各有优缺点。

fs.watch使用文件系统事件通知文件及目录的变化。 但是它依赖于操作系统,且事件通知并不完全可靠,在很多操作系统上的行为难以预料。 还可能会有创建监视个数的限制,如Linux系统,在包含大量文件的程序中监视器个数很快被耗尽。 但也正是因为它使用文件系统事件,不需要占用过多的CPU周期。 典型地,编译器使用fs.watch来监视目录(比如配置文件里声明的源码目录,无法进行模块解析的目录)。 这样就可以处理改动通知不准确的问题。 但递归地监视仅在Windows和OSX系统上支持。 这就意味着在其它系统上要使用替代方案。

fs.watchFile使用轮询,因此涉及到CPU周期。 但是这是最可靠的获取文件/目录状态的机制。 典型地,编译器使用fs.watchFile监视源文件,配置文件和消失的文件(失去文件引用),这意味着对CPU的使用依赖于程序里文件的数量。

在MSBuild里使用编译选项

概述

编译选项可以在使用MSBuild的项目里通过MSBuild属性指定。

例子

  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <TypeScriptRemoveComments>false</TypeScriptRemoveComments>
    <TypeScriptSourceMap>true</TypeScriptSourceMap>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <TypeScriptRemoveComments>true</TypeScriptRemoveComments>
    <TypeScriptSourceMap>false</TypeScriptSourceMap>
  </PropertyGroup>
  <Import
      Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets"
      Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')" />

映射

编译选项MSBuild属性名称可用值
--allowJsMSBuild不支持此选项
--allowSyntheticDefaultImportsTypeScriptAllowSyntheticDefaultImports布尔值
--allowUnreachableCodeTypeScriptAllowUnreachableCode布尔值
--allowUnusedLabelsTypeScriptAllowUnusedLabels布尔值
--alwaysStrictTypeScriptAlwaysStrict布尔值
--baseUrlTypeScriptBaseUrl文件路径
--charsetTypeScriptCharset
--declarationTypeScriptGeneratesDeclarations布尔值
--declarationDirTypeScriptDeclarationDir文件路径
--diagnosticsMSBuild不支持此选项
--disableSizeLimitMSBuild不支持此选项
--emitBOMTypeScriptEmitBOM布尔值
--emitDecoratorMetadataTypeScriptEmitDecoratorMetadata布尔值
--experimentalAsyncFunctionsTypeScriptExperimentalAsyncFunctions布尔值
--experimentalDecoratorsTypeScriptExperimentalDecorators布尔值
--forceConsistentCasingInFileNamesTypeScriptForceConsistentCasingInFileNames布尔值
--helpMSBuild不支持此选项
--importHelpersTypeScriptImportHelpers布尔值
--inlineSourceMapTypeScriptInlineSourceMap布尔值
--inlineSourcesTypeScriptInlineSources布尔值
--initMSBuild不支持此选项
--isolatedModulesTypeScriptIsolatedModules布尔值
--jsxTypeScriptJSXEmitreactreact-nativepreserve
--jsxFactoryTypeScriptJSXFactory有效的名字
--libTypeScriptLib逗号分隔的字符串列表
--listEmittedFilesMSBuild不支持此选项
--listFilesMSBuild不支持此选项
--localeautomatic自动设置为PreferredUILang值
--mapRootTypeScriptMapRoot文件路径
--maxNodeModuleJsDepthMSBuild不支持此选项
--moduleTypeScriptModuleKindAMDCommonJsUMDSystemES6
--moduleResolutionTypeScriptModuleResolutionClassicNode
--newLineTypeScriptNewLineCRLFLF
--noEmitMSBuild不支持此选项
--noEmitHelpersTypeScriptNoEmitHelpers布尔值
--noEmitOnErrorTypeScriptNoEmitOnError布尔值
--noFallthroughCasesInSwitchTypeScriptNoFallthroughCasesInSwitch布尔值
--noImplicitAnyTypeScriptNoImplicitAny布尔值
--noImplicitReturnsTypeScriptNoImplicitReturns布尔值
--noImplicitThisTypeScriptNoImplicitThis布尔值
--noImplicitUseStrictTypeScriptNoImplicitUseStrict布尔值
--noStrictGenericChecksTypeScriptNoStrictGenericChecks布尔值
--noUnusedLocalsTypeScriptNoUnusedLocals布尔值
--noUnusedParametersTypeScriptNoUnusedParameters布尔值
--noLibTypeScriptNoLib布尔值
--noResolveTypeScriptNoResolve布尔值
--outTypeScriptOutFile文件路径
--outDirTypeScriptOutDir文件路径
--outFileTypeScriptOutFile文件路径
--pathsMSBuild不支持此选项
--preserveConstEnumsTypeScriptPreserveConstEnums布尔值
--preserveSymlinksTypeScriptPreserveSymlinks布尔值
--listEmittedFilesMSBuild不支持此选项
--prettyMSBuild不支持此选项
--reactNamespaceTypeScriptReactNamespace字符串
--removeCommentsTypeScriptRemoveComments布尔值
--rootDirTypeScriptRootDir文件路径
--rootDirsMSBuild不支持此选项
--skipLibCheckTypeScriptSkipLibCheck布尔值
--skipDefaultLibCheckTypeScriptSkipDefaultLibCheck布尔值
--sourceMapTypeScriptSourceMap文件路径
--sourceRootTypeScriptSourceRoot文件路径
--strictTypeScriptStrict布尔值
--strictFunctionTypesTypeScriptStrictFunctionTypes布尔值
--strictNullChecksTypeScriptStrictNullChecks布尔值
--stripInternalTypeScriptStripInternal布尔值
--suppressExcessPropertyErrorsTypeScriptSuppressExcessPropertyErrors布尔值
--suppressImplicitAnyIndexErrorsTypeScriptSuppressImplicitAnyIndexErrors布尔值
--targetTypeScriptTargetES3ES5,或ES6
--traceResolutionMSBuild不支持此选项
--typesMSBuild不支持此选项
--typeRootsMSBuild不支持此选项
--watchMSBuild不支持此选项
MSBuild only optionTypeScriptAdditionalFlags任何编译选项

我使用的Visual Studio版本里支持哪些选项?

查找 C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets 文件。 可用的MSBuild XML标签与相应的tsc编译选项的映射都在那里。

ToolsVersion

工程文件里的<TypeScriptToolsVersion>1.7</TypeScriptToolsVersion>属性值表明了构建时使用的编译器的版本号(这个例子里是1.7) 这样就允许一个工程在不同的机器上使用相同版本的编译器进行构建。

如果没有指定TypeScriptToolsVersion,则会使用机器上安装的最新版本的编译器去构建。

如果用户使用的是更新版本的TypeScript,则会在首次加载工程的时候看到一个提示升级工程的对话框。

TypeScriptCompileBlocked

如果你使用其它的构建工具(比如,gulp, grunt等等)并且使用VS做为开发和调试工具,那么在工程里设置<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>。 这样VS只会提供给你编辑的功能,而不会在你按F5的时候去构建。

与其它构建工具整合

构建工具

Babel

安装

npm install @babel/cli @babel/core @babel/preset-typescript --save-dev

.babelrc

{
  "presets": ["@babel/preset-typescript"]
}

使用命令行工具

./node_modules/.bin/babel --out-file bundle.js src/index.ts

package.json

{
  "scripts": {
    "build": "babel --out-file bundle.js main.ts"
  },
}

在命令行上运行Babel

npm run build

Browserify

安装

npm install tsify

使用命令行交互

browserify main.ts -p [ tsify --noImplicitAny ] > bundle.js

使用API

var browserify = require("browserify");
var tsify = require("tsify");

browserify()
    .add('main.ts')
    .plugin('tsify', { noImplicitAny: true })
    .bundle()
    .pipe(process.stdout);

更多详细信息:smrq/tsify

Duo

安装

npm install duo-typescript

使用命令行交互

duo --use duo-typescript entry.ts

使用API

var Duo = require('duo');
var fs = require('fs')
var path = require('path')
var typescript = require('duo-typescript');

var out = path.join(__dirname, "output.js")

Duo(__dirname)
    .entry('entry.ts')
    .use(typescript())
    .run(function (err, results) {
        if (err) throw err;
        // Write compiled result to output file
        fs.writeFileSync(out, results.code);
    });

更多详细信息:frankwallis/duo-typescript

Grunt

安装

npm install grunt-ts

基本Gruntfile.js

module.exports = function(grunt) {
    grunt.initConfig({
        ts: {
            default : {
                src: ["**/*.ts", "!node_modules/**/*.ts"]
            }
        }
    });
    grunt.loadNpmTasks("grunt-ts");
    grunt.registerTask("default", ["ts"]);
};

更多详细信息:TypeStrong/grunt-ts

Gulp

安装

npm install gulp-typescript

基本gulpfile.js

var gulp = require("gulp");
var ts = require("gulp-typescript");

gulp.task("default", function () {
    var tsResult = gulp.src("src/*.ts")
        .pipe(ts({
              noImplicitAny: true,
              out: "output.js"
        }));
    return tsResult.js.pipe(gulp.dest('built/local'));
});

更多详细信息:ivogabe/gulp-typescript

Jspm

安装

npm install -g jspm@beta

注意:目前jspm的0.16beta版本支持TypeScript

更多详细信息:TypeScriptSamples/jspm

Webpack

安装

npm install ts-loader --save-dev

Webpack 2 webpack.config.js 基础配置

module.exports = {
    entry: "./src/index.tsx",
    output: {
        path: '/',
        filename: "bundle.js"
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js", ".json"]
    },
    module: {
        rules: [
            // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'
            { test: /\.tsx?$/, use: ["ts-loader"], exclude: /node_modules/ }
        ]
    }
}

Webpack 1 webpack.config.js 基础配置

module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "bundle.js"
    },
    resolve: {
        // Add '.ts' and '.tsx' as a resolvable extension.
        extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
    },
    module: {
        loaders: [
            // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'
            { test: /\.tsx?$/, loader: "ts-loader" }
        ]
    }
};

查看更多关于ts-loader的详细信息

或者

MSBuild

更新工程文件,包含本地安装的Microsoft.TypeScript.Default.props(在顶端)和Microsoft.TypeScript.targets(在底部)文件:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- Include default props at the top -->
  <Import
      Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props"
      Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props')" />

  <!-- TypeScript configurations go here -->
  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <TypeScriptRemoveComments>false</TypeScriptRemoveComments>
    <TypeScriptSourceMap>true</TypeScriptSourceMap>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <TypeScriptRemoveComments>true</TypeScriptRemoveComments>
    <TypeScriptSourceMap>false</TypeScriptSourceMap>
  </PropertyGroup>

  <!-- Include default targets at the bottom -->
  <Import
      Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets"
      Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')" />
</Project>

关于配置MSBuild编译器选项的更多详细信息,请参考:在MSBuild里使用编译选项

NuGet

  • 右键点击 -> Manage NuGet Packages
  • 查找Microsoft.TypeScript.MSBuild
  • 点击Install
  • 安装完成后,Rebuild。

更多详细信息请参考Package Manager Dialogusing nightly builds with NuGet

在太平洋标准时间的每日午夜,TypeScript 代码仓库中master 分支上的代码会自动构建并发布到 npm 上。 下面将介绍如何获取并结合你的工具来使用它。

使用 npm

npm install -g typescript@next

更新 IDE 来使用每日构建

你还可以配置 IDE 来使用每日构建。 首先你需要通过 npm 来安装代码包。 你可以进行全局安装或者安装到本地的node_modules目录下。

在下面的内容中,我们假设你已经安装好了typescript@next

Visual Studio Code

参考以下示例来更新.vscode/settings.json

"typescript.tsdk": "<path to your folder>/node_modules/typescript/lib"

更多详情请参考 VSCode 文档

Sublime Text

参考以下示例来更新Settings - User

"typescript_tsdk": "<path to your folder>/node_modules/typescript/lib"

更多详情请参考 如何在 Sublime Text 里安装 TypeScript 插件

Visual Studio 2013 和 2015

注意:绝大多数的变更不需要你安装新版本的 VS TypeScript 插件。

目前,每日构建中没有包含完整的插件安装包,但是我们正在试着提供这样的安装包。

  1. 下载 VSDevMode.ps1 脚本。

    同时也可以参考 wiki 文档: 使用自定义的语言服务文件

  2. 打开 PowerShell 命令行窗口,并运行:

针对 VS 2015:

VSDevMode.ps1 14 -tsScript <path to your folder>/node_modules/typescript/lib

针对 VS 2013:

VSDevMode.ps1 12 -tsScript <path to your folder>/node_modules/typescript/lib

IntelliJ IDEA (Mac)

前往 Preferences > Languages & Frameworks > TypeScript

TypeScript Version:若通过 npm 安装则为:/usr/local/lib/node_modules/typescript/lib

IntelliJ IDEA (Windows)

前往 File > Settings > Languages & Frameworks > TypeScript

TypeScript Version:若通过 npm 安装则为:C:\Users\USERNAME\AppData\Roaming\npm\node_modules\typescript\lib

新增功能

TypeScript 5.4

从最后一次赋值以后,在闭包中保留类型细化

TypeScript 通常可以根据您进行的检查来确定变量更具体的类型。 这个过程被称为类型细化。

function uppercaseStrings(x: string | number) {
  if (typeof x === 'string') {
    // TypeScript 知道 'x' 是 'string' 类型
    return x.toUpperCase();
  }
}

一个常见的痛点是被细化的类型不总会在闭包函数中保留。

function getUrls(url: string | URL, names: string[]) {
  if (typeof url === 'string') {
    url = new URL(url);
  }

  return names.map(name => {
    url.searchParams.set('name', name);
    //  ~~~~~~~~~~~~
    // error!
    // Property 'searchParams' does not exist on type 'string | URL'.

    return url.toString();
  });
}

在这里,TypeScript 决定在我们的回调函数中不“安全”地假设 url 实际上是一个 URL 对象,因为它在其他地方发生了变化; 然而,在这种情况下,箭头函数总是在对 url 的赋值之后创建的,并且它也是对 url 的最后一次赋值。

TypeScript 5.4 利用这一点使类型细化变得更加智能。 当在非提升的函数中使用参数和 let 变量时,类型检查器将寻找最后一次赋值点。 如果找到了这样的点,TypeScript 可以安全地从包含函数的外部进行类型细化。 这意味着上面的例子现在可以正常工作了。

请注意,如果变量在嵌套函数的任何地方被赋值,类型细化分析将不会生效。 这是因为无法确定该函数是否会在后续被调用。

function printValueLater(value: string | undefined) {
  if (value === undefined) {
    value = 'missing!';
  }

  setTimeout(() => {
    // Modifying 'value', even in a way that shouldn't affect
    // its type, will invalidate type refinements in closures.
    value = value;
  }, 500);

  setTimeout(() => {
    console.log(value.toUpperCase());
    //          ~~~~~
    // error! 'value' is possibly 'undefined'.
  }, 1000);
}

这将使许多典型的 JavaScript 代码更容易表达出来。 更多详情请参考PR

NoInfer 工具类型

当调用泛型函数时,TypeScript 能够从实际参数推断出类型参数的值。

function doSomething<T>(arg: T) {
  // ...
}

// We can explicitly say that 'T' should be 'string'.
doSomething<string>('hello!');

// We can also just let the type of 'T' get inferred.
doSomething('hello!');

然而,一个挑战是并不总能够清楚推断出“最佳”的类型是什么。 这可能导致 TypeScript 拒绝合理的调用,接受有问题的调用,或者在捕捉到 bug 时报告较差的错误消息。

例如,假设 createStreetLight 函数接收一系列颜色名,以及一个默认颜色名。

function createStreetLight<C extends string>(colors: C[], defaultColor?: C) {
  // ...
}

createStreetLight(['red', 'yellow', 'green'], 'red');

当我们传入的 defaultColor 不在 colors 数组里会发生什么? 在这个函数中,colors 被当成“事实来源”,并描述了可以传递给 defaultColor

// Oops! This undesirable, but is allowed!
createStreetLight(['red', 'yellow', 'green'], 'blue');

在这个调用中,类型推断决定 "blue""red""yellow""green" 一样有效。 因此,TypeScript 推断 C 的类型为 "red" | "yellow" | "green" | "blue"。 可以说推断结果让我们感到十分惊讶!

目前人们处理这个问题的一种方式是添加一个独立的类型参数,该参数受现有类型参数的限制。

function createStreetLight<C extends string, D extends C>(
  colors: C[],
  defaultColor?: D
) {}

createStreetLight(['red', 'yellow', 'green'], 'blue');
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.

这种方法可以解决问题,但有点尴尬,因为在 createStreetLight 的签名中可能不会在其他地方使用 D。 虽然这种情况不算糟糕,但在签名中只使用一次类型参数通常是一种代码坏味道。

这就是为什么 TypeScript 5.4 引入了一个新的 NoInfer<T> 实用类型。 将一个类型包裹在 NoInfer<...> 中向 TypeScript 发出一个信号,告诉它不要深入匹配内部类型以寻找类型推断的候选项。

使用 NoInfer,我们可以将 createStreetLight 重写为以下形式:

function createStreetLight<C extends string>(
  colors: C[],
  defaultColor?: NoInfer<C>
) {
  // ...
}

createStreetLight(['red', 'yellow', 'green'], 'blue');
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.

排除对 defaultColor 类型进行推断的探索意味着 "blue" 永远不会成为推断的候选项,类型检查器可以拒绝它。

具体实现请参考 PR,以及最初实现 PR,感谢Mateusz Burzyński

Object.groupByMap.groupBy

TypeScript 5.4 为 JavaScript 的新静态方法 Object.groupByMap.groupBy 添加了声明。

Object.groupBy 接受一个可迭代对象和一个函数,该函数确定每个元素应该被放置在哪个“组”中。 该函数需要为每个不同的分组生成一个“键”,而 Object.groupBy 使用该键来创建一个对象,其中每个键都映射到一个包含原始元素的数组。

因此:

const array = [0, 1, 2, 3, 4, 5];

const myObj = Object.groupBy(array, (num, index) => {
  return num % 2 === 0 ? 'even' : 'odd';
});

等同于:

const myObj = {
  even: [0, 2, 4],
  odd: [1, 3, 5],
};

Map.groupBy 类似,但生成的是一个 Map 而不是普通对象。 如果您需要 Map 提供的保证、处理期望 Map 的 API,或者需要使用任何类型的键进行分组(而不仅仅是可以作为 JavaScript 属性名的键),那么这可能更可取。

const myObj = Map.groupBy(array, (num, index) => {
  return num % 2 === 0 ? 'even' : 'odd';
});

就像之前一样,你可以用等效的方式创建 myObj

const myObj = new Map();

myObj.set('even', [0, 2, 4]);
myObj.set('odd', [1, 3, 5]);

注意在 Object.groupBy 的例子中,生成的对象使用了所有可选属性。

interface EvenOdds {
    even?: number[];
    odd?: number[];
}

const myObj: EvenOdds = Object.groupBy(...);

myObj.even;
//    ~~~~
// Error to access this under 'strictNullChecks'.

这是因为没法保证 groupBy 生成了所有的键。

注意这些方法仅在将 target 设置为 esnext,或者设置了相应的 lib 选项时才可用。 我们预计它们最终会在稳定的 es2024 目标下可用。

感谢Kevin GibbonsPR

支持在 --moduleResolution bundler--module preserve 时 使用 require()

TypeScript 有一个名为 bundlermoduleResolution 选项,旨在模拟现代打包工具确定导入路径所指向的文件的方式。 该选项的一个限制是它必须与 --module esnext 配对使用,这导致无法使用 import ... = require(...) 语法。

// previously errored
import myModule = require('module/path');

如果您计划只编写标准的 ECMAScript import,这可能看起来并不是很重要,但在使用具有条件导出的包时就会有所不同。

在 TypeScript 5.4 中,当将 module 设置为一个名为 preserve 的新选项时,可以使用 require()

--module preserve--moduleResolution bundler 之间,这两个选项更准确地模拟了像 Bun 等打包工具和运行时环境允许的操作以及它们如何执行模块查找。 实际上,在使用 --module preserve 时,--moduleResolution 选项将会隐式设置为 bundler(以及 --esModuleInterop--resolveJsonModule)。

{
  "compilerOptions": {
    "module": "preserve"
    // ^ also implies:
    // "moduleResolution": "bundler",
    // "esModuleInterop": true,
    // "resolveJsonModule": true,

    // ...
  }
}

--module preserve 下,ECMAScript 的 import 语句将始终按原样输出,而 import ... = require(...) 语句将被输出为 require() 调用(尽管实际上你可能不会使用 TypeScript 进行输出,因为你很可能会使用打包工具来处理你的代码)。 这一点不受包含文件的文件扩展名的影响。 因此,以下代码:

import * as foo from 'some-package/foo';
import bar = require('some-package/bar');

的输出结果会是这样:

import * as foo from 'some-package/foo';
var bar = require('some-package/bar');

这也意味着您选择的语法将指定条件导出的匹配方式。 因此,在上面的示例中,如果 some-packagepackage.json 如下所示:

{
  "name": "some-package",
  "version": "0.0.1",
  "exports": {
    "./foo": {
      "import": "./esm/foo-from-import.mjs",
      "require": "./cjs/foo-from-require.cjs"
    },
    "./bar": {
      "import": "./esm/bar-from-import.mjs",
      "require": "./cjs/bar-from-require.cjs"
    }
  }
}

TypeScript 会将路径解析为 [...]/some-package/esm/foo-from-import.mjs[...]/some-package/cjs/bar-from-require.cjs

更多详情请参考 PR

检查导入属性和断言

导入属性和断言现在会与全局的 ImportAttributes 类型进行检查。 这意味着运行时现在可以更准确地描述导入属性。

// In some global file.
interface ImportAttributes {
    type: "json";
}

// In some other module
import * as ns from "foo" with { type: "not-json" };
//                                     ~~~~~~~~~~
// error!
//
// Type '{ type: "not-json"; }' is not assignable to type 'ImportAttributes'.
//  Types of property 'type' are incompatible.
//    Type '"not-json"' is not assignable to type '"json"'.

感谢 Oleksandr TarasiukPR

快速修复:添加缺失参数

TypeScript 现在提供了一个快速修复选项,可以为被调用时传递了过多参数的函数添加一个新的参数。

当在多个现有函数之间传递一个新参数时,这将非常有用,而目前这样做可能会很麻烦。

感谢 Oleksandr TarasiukPR

子路径导入支持自动导入

在 Node.js 中,package.json 通过一个名为 imports 的字段支持一种称为“子路径导入”的功能。 这是一种将包内的路径重新映射到其他模块路径的方式。 在概念上,这与路径映射非常相似,某些模块打包工具和加载器支持该功能(TypeScript 通过一个称为 paths 的功能也支持该功能)。 唯一的区别是,子路径导入必须始终以 # 开头。

TypeScript 的自动导入功能以前不会考虑 imports 中的路径,这可能令人沮丧。 相反,用户可能需要在 tsconfig.json 中手动定义路径。 然而,由于 Emma Hamilton 的贡献,TypeScript 的自动导入现在支持子路径导入

即将到来的 TypeScript 5.0 弃用功能

TypeScript 5.0 弃用了以下选项和行为:

  • charset
  • target: ES3
  • importsNotUsedAsValues
  • noImplicitUseStrict
  • noStrictGenericChecks
  • keyofStringsOnly
  • suppressExcessPropertyErrors
  • suppressImplicitAnyIndexErrors
  • out
  • preserveValueImports
  • 工程引用中的 prepend
  • 隐式的系统特定 newLine

为了继续使用这些功能,使用 TypeScript 5.0 + 版本的开发人员必须指定一个名为 ignoreDeprecations 的新选项,其值为 "5.0"

然而,TypScript 5.4 将是这些功能继续正常工作的最后一个版本。 到了 TypeScript 5.5(可能是 2024 年 6 月),它们将变成严格的错误,使用它们的代码将需要进行迁移。

要获取更多信息,您可以在 GitHub 上查阅这个计划,其中包含了如何最佳地适应您的代码库的建议。

值得注意的行为改变

本节重点介绍一系列值得注意的变更,作为升级的一部分,应该予以认识和理解。 有时它会强调弃用、移除和新的限制。 它还可能包含功能性改进的错误修复,但这些修复也可能通过引入新的错误影响现有的构建。

lib.d.ts 变化

DOM 类型有变化

更准确的有条件类型约束

下面的 foo 函数不再允许第二个变量声明。

type IsArray<T> = T extends any[] ? true : false;

function foo<U extends object>(x: IsArray<U>) {
  let first: true = x; // Error
  let second: false = x; // Error, but previously wasn't
}

在之前的版本中,当 TypeScript 检查第二个初始化器时,它需要确定 IsArray<U> 是否可赋值给 false 类型的单元类型。 虽然 IsArray<U> 在任何明显的方式下都不兼容,但 TypeScript 也会考虑该类型的约束。 在形如 T extends Foo ? TrueBranch : FalseBranch 的条件类型中,其中 T 是泛型,类型系统会查看 T 的约束,在 T 本身的位置上进行替代,并决定选择 true 分支还是 false 分支。

但是,这种行为是不准确的,因为它过于急切。即使 T 的约束不能赋值给 Foo,也并不意味着它不会实例化为某个可赋值给 Foo 的类型。 因此,更正确的行为是在无法证明 T 永远不会或总是 extends Foo 的情况下,为条件类型的约束产生一个联合类型。

TypeScript 5.4 采用了这种更准确的行为。 在实践中,这意味着您可能会发现某些条件类型实例与它们的分支不再兼容。

您可以在此处阅读具体的更改内容。

更积极地减少类型变量与原始类型之间的交集

declare function intersect<T, U>(x: T, y: U): T & U;

function foo<T extends 'abc' | 'def'>(x: T, str: string, num: number) {
  // Was 'T & string', now is just 'T'
  let a = intersect(x, str);

  // Was 'T & number', now is just 'never'
  let b = intersect(x, num);

  // Was '(T & "abc") | (T & "def")', now is just 'T'
  let c = Math.random() < 0.5 ? intersect(x, 'abc') : intersect(x, 'def');
}

更多详情请参考 PR

改进了对带有插值的模板字符串的检查

TypeScript 现在更准确地检查字符串是否可赋值给模板字符串类型的占位符位置。

function a<T extends { id: string }>() {
  let x: `-${keyof T & string}`;

  // Used to error, now doesn't.
  x = '-id';
}

这种行为更加理想,但可能会导致使用条件类型等结构的代码出现问题,因为这些规则变化很容易引发观察到的错误。

更多详情请参考 PR

当类型导入与本地值冲突时报错

在之前的版本中,如果对 "Something" 的导入只涉及类型,TypeScript 会在 "isolatedModules" 下允许以下代码。

import { Something } from './some/path';

let Something = 123;

然而,对于单文件编译器来说,假设是否能够"安全"删除 import 并不可靠,即使代码在运行时肯定会失败。 在 TypeScript 5.4 中,这段代码将触发以下类似的错误:

Import 'Something' conflicts with local value, so must be declared with a type-only import when 'isolatedModules' is enabled.

修改方法或者给本地变量重命名,或者为导入语句添加 type 修饰符:

import type { Something } from './some/path';

// or

import { type Something } from './some/path';

更多详情请参考 PR

新的枚举可赋值性检查

在之前的版本中,当两个枚举具有相同的声明名称和枚举成员名称时,它们通常被认为是兼容的。 然而,当这些值是已知的时候,TypeScript 会默默地允许它们具有不同的值。

TypeScript 5.4 通过要求已知的值必须相同来加强这一限制。 这意味着当枚举的值已知时,它们必须具有相同的值。

namespace First {
  export enum SomeEnum {
    A = 0,
    B = 1,
  }
}

namespace Second {
  export enum SomeEnum {
    A = 0,
    B = 2,
  }
}

function foo(x: First.SomeEnum, y: Second.SomeEnum) {
  // Both used to be compatible - no longer the case,
  // TypeScript errors with something like:
  //
  //  Each declaration of 'SomeEnum.B' differs in its value, where '1' was expected but '2' was given.
  x = y;
  y = x;
}

此外,对于一个枚举成员没有静态已知值的情况,还有一些新的限制。 在这些情况下,另一个枚举成员必须至少是隐式数字类型(例如,它没有静态解析的初始化值),或者是显式数字类型(意味着 TypeScript 可以将值解析为某个数字类型)。 从实际角度来看,这意味着字符串枚举成员只能与具有相同值的其他字符串枚举兼容。

namespace First {
  export declare enum SomeEnum {
    A,
    B,
  }
}

namespace Second {
  export declare enum SomeEnum {
    A,
    B = 'some known string',
  }
}

function foo(x: First.SomeEnum, y: Second.SomeEnum) {
  // Both used to be compatible - no longer the case,
  // TypeScript errors with something like:
  //
  //  One value of 'SomeEnum.B' is the string '"some known string"', and the other is assumed to be an unknown numeric value.
  x = y;
  y = x;
}

更多详情请参考 PR

枚举成员名的限制

TypeScript 不再允许枚举成员名使用 Infinity-Infinity,或 NaN

// Errors on all of these:
//
//  An enum member cannot have a numeric name.
enum E {
  Infinity = 0,
  '-Infinity' = 1,
  NaN = 2,
}

更多详情请参考 PR

在具有 any 剩余元素的元组上,更好地保留映射类型

在之前的版本中,将带有 "any" 类型的映射类型应用于元组时,会创建一个 "any" 元素类型。 这是不可取的,并且现在已经修复了这个问题。

Promise.all(['', ...([] as any)]).then(result => {
  const head = result[0]; // 5.3: any, 5.4: string
  const tail = result.slice(1); // 5.3 any, 5.4: any[]
});

更多详情请参考 PRIssueIssue

代码生成变化

虽然这不是一个直接的破坏性变更,但开发人员可能会隐式地依赖于 TypeScript 生成的 JavaScript 或声明文件输出。以下是一些值得注意的变化。

TypeScript 5.3

导入属性(Import Attributes)

TypeScript 5.3 支持了最新的 import attributes 提案。

该特性的一个用例是为运行时提供期望的模块格式信息。

// We only want this to be interpreted as JSON,
// not a runnable/malicious JavaScript file with a `.json` extension.
import obj from "./something.json" with { type: "json" };

TypeScript 不会检查属性内容,因为它们是宿主环境相关的。 TypeScript 会原样保留它们,浏览器和运行时会处理它们。

// TypeScript is fine with this.
// But your browser? Probably not.
import * as foo from "./foo.js" with { type: "fluffy bunny" };

动态的 import() 调用也可以在第二个参数里使用该特性。

const obj = await import('./something.json', {
  with: { type: 'json' },
});

第二个参数的期望类型为 ImportCallOptions,默认只支持一个名为 with 的属性。

请注意,导入属性是之前提案“导入断言”的演进,该提案已在 TypeScript 4.5 中实现。 最明显的区别是使用with关键字而不是assert关键字。 但不太明显的区别是,现在运行时可以自由地使用属性来指导导入路径的解析和解释,而导入断言只能在加载模块后断言某些特性。

随着时间的推移,TypeScript 将逐渐弃用旧的导入断言语法,转而采用导入属性的提议语法。现有的使用assert的代码应该迁移到with关键字。而需要导入属性的新代码应该完全使用with关键字。

感谢 Oleksandr Tarasiuk 实现了这个功能! 也感谢 Wenlu Wang 实现了 import assertions!

稳定支持 import type 上的 resolution-mode

TypeScript 4.7 在 /// <reference types="..." /> 里支持了 resolution-mode 属性, 它用来控制一个描述符是使用 import 还是 require 语义来解析。

/// <reference types="pkg" resolution-mode="require" />

// or

/// <reference types="pkg" resolution-mode="import" />

在 type-only 导入上,导入断言也引入了相应的字段; 然而,它仅在 TypeScript 的夜间版本中得到支持 其原因是在精神上,导入断言并不打算指导模块解析。 因此,这个特性以实验性的方式仅在夜间版本中发布,以获得更多的反馈。

但是,导入属性(Import Attributes)可以指导解析,并且我们也已经看到了有意义的用例, TypeScript 5.3 在 import type 上支持了 resolution-mode

// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from "pkg" with {
    "resolution-mode": "require"
};

// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from "pkg" with {
    "resolution-mode": "import"
};

export interface MergedType extends TypeFromRequire, TypeFromImport {}

这些导入属性也可以用在 import() 类型上。

export type TypeFromRequire =
    import("pkg", { with: { "resolution-mode": "require" } }).TypeFromRequire;

export type TypeFromImport =
    import("pkg", { with: { "resolution-mode": "import" } }).TypeFromImport;

export interface MergedType extends TypeFromRequire, TypeFromImport {}

更多详情,请参考PR

在所有模块模式中支持 resolution-mode

此前,仅在 moduleResolutionnode16nodenext 时支持 resolution-mode。 为了使查找模块更容易,尤其针对类型,resolution-mode 现在可以在所有其它的 moduleResolution 选项下工作,例如 bundlernode10,甚至在 classic 下也不报错。

更多详情,请参考PR

switch (true) 类型细化

TypeScript 5.3 会针对 switch (true) 里的每一个 case 条件进行类型细化。

function f(x: unknown) {
  switch (true) {
    case typeof x === 'string':
      // 'x' is a 'string' here
      console.log(x.toUpperCase());
    // falls through...

    case Array.isArray(x):
      // 'x' is a 'string | any[]' here.
      console.log(x.length);
    // falls through...

    default:
    // 'x' is 'unknown' here.
    // ...
  }
}

感谢 Mateusz Burzyński 的贡献

类型细化与布尔值的比较

有时,您可能会发现自己在条件语句中直接与 truefalse 进行比较。 通常情况下,这些比较是不必要的,但您可能出于风格上的考虑或为了避免 JavaScript 中真值相关的某些问题而偏好这样做。 不过,之前 TypeScript 在进行类型细化时并不识别这样的形式。

TypeScript 5.3 在类型细化时可以理解这类表达式。

interface A {
  a: string;
}

interface B {
  b: string;
}

type MyType = A | B;

function isA(x: MyType): x is A {
  return 'a' in x;
}

function someFn(x: MyType) {
  if (isA(x) === true) {
    console.log(x.a); // works!
  }
}

感谢 Mateusz Burzyński 的 PR

利用 Symbol.hasInstance 来细化 instanceof

JavaScript 的一个稍微晦涩的特性是可以覆盖 instanceof 运算符的行为。 为此,instanceof 运算符右侧的值需要具有一个名为 Symbol.hasInstance 的特定方法。

class Weirdo {
  static [Symbol.hasInstance](testedValue) {
    // wait, what?
    return testedValue === undefined;
  }
}

// false
console.log(new Thing() instanceof Weirdo);

// true
console.log(undefined instanceof Weirdo);

为了更好地支持 instanceof 的行为,TypeScript 现在会检查是否存在 [Symbol.hasInstance] 方法且被定义为类型判定函数。 如果有的话,instanceof 运算符左侧的值会按照类型判定进行细化。

interface PointLike {
  x: number;
  y: number;
}

class Point implements PointLike {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  distanceFromOrigin() {
    return Math.sqrt(this.x ** 2 + this.y ** 2);
  }

  static [Symbol.hasInstance](val: unknown): val is PointLike {
    return (
      !!val &&
      typeof val === 'object' &&
      'x' in val &&
      'y' in val &&
      typeof val.x === 'number' &&
      typeof val.y === 'number'
    );
  }
}

function f(value: unknown) {
  if (value instanceof Point) {
    // Can access both of these - correct!
    value.x;
    value.y;

    // Can't access this - we have a 'PointLike',
    // but we don't *actually* have a 'Point'.
    value.distanceFromOrigin();
  }
}

能够看到例子中,Point 定义了自己的 [Symbol.hasInstance] 方法。 它实际上充当了对称为 PointLike 的单独类型的自定义类型保护。 在函数 f 中,我们能够使用 instanceofvalue 细化为 PointLike,但不能细化到 Point。 这意味着我们可以访问属性 xy,但无法访问 distanceFromOrigin 方法。

更多详情请参考PR

在实例字段上检查 super 属性访问

在 JavaScript 中,能够使用 super 关键字来访问基类中的声明。

class Base {
  someMethod() {
    console.log('Base method called!');
  }
}

class Derived extends Base {
  someMethod() {
    console.log('Derived method called!');
    super.someMethod();
  }
}

new Derived().someMethod();
// Prints:
//   Derived method called!
//   Base method called!

这与 this.someMethod() 是不同的,因为它可能调用的是重写的方法。 这是一个微妙的区别,而且通常情况下,如果一个声明从未被覆盖,这两者可以互换,使得区别更加微妙。

class Base {
  someMethod() {
    console.log('someMethod called!');
  }
}

class Derived extends Base {
  someOtherMethod() {
    // These act identically.
    this.someMethod();
    super.someMethod();
  }
}

new Derived().someOtherMethod();
// Prints:
//   someMethod called!
//   someMethod called!

将它们互换使用的问题在于,super 关键字仅适用于在原型上声明的成员,而不适用于实例属性。 这意味着,如果您编写了 super.someMethod(),但 someMethod 被定义为一个字段,那么您将会得到一个运行时错误!

class Base {
  someMethod = () => {
    console.log('someMethod called!');
  };
}

class Derived extends Base {
  someOtherMethod() {
    super.someMethod();
  }
}

new Derived().someOtherMethod();
//
// Doesn't work because 'super.someMethod' is 'undefined'.

TypeScript 5.3 现在更仔细地检查 super 属性访问/方法调用,以确定它们是否对应于类字段。 如果是这样,我们现在将会得到一个类型检查错误。

这个检查是由 Jack Works 开发!

可以交互的类型内嵌提示

TypeScript 的内嵌提示支持跳转到类型定义! 这便利在代码间跳转变得简单。

更多详情请参考PR

设置偏好 type 自动导入

之前,当 TypeScript 为类型自动生成导入语句时,它会根据配置添加 type 修饰符。 例如,当为 Person 生成自动导入语句时:

export let p: Person;

TypeScript 通常会这样生成 Person 导入:

import { Person } from './types';

export let p: Person;

如果设置了 verbatimModuleSyntax,它会添加 type 修饰符:

import { type Person } from './types';

export let p: Person;

然而,也许你的编辑器不支持这些选项;或者你偏好显式地使用 type 导入。

最近的一项改动,TypeScript 把它变成了针对编辑器的配置项。 在 Visual Studio Code 中,你可以在 "TypeScript › Preferences: Prefer Type Only Auto Imports" 启用该功能,或者在 JSON 配置文件中的 typescript.preferences.preferTypeOnlyAutoImports 设置。

优化:略过 JSDoc 解析

当通过 tsc 运行 TypeScript 时,编译器现在将避免解析 JSDoc。 这不仅减少了解析时间,还减少了存储注释以及垃圾回收所花费的内存使用量。 总体而言,您应该会看到编译速度稍微更快,并在 --watch 模式下获得更快的反馈。

具体改动在这

由于并非每个使用 TypeScript 的工具都需要存储 JSDoc(例如 typescript-eslint 和 Prettier),因此这种解析策略已作为 API 的一部分公开。 这使得这些工具能够获得与 TypeScript 编译器相同的内存和速度改进。 注释解析策略的新选项在 JSDocParsingMode 中进行了描述。 关于此拉取请求的更多信息,请参阅PR

通过比较非规范化的交叉类型进行优化

在 TypeScript 中,联合类型和交叉类型始终遵循特定的形式,其中交叉类型不能包含联合类型。 这意味着当我们在一个联合类型上创建一个交叉类型,例如 A & (B | C),该交叉类型将被规范化为 (A & B) | (A & C)。 然而,在某些情况下,类型系统会保留原始形式以供显示目的使用。

事实证明,原始形式可以用于一些巧妙的快速路径类型比较。

例如,假设我们有 SomeType & (Type1 | Type2 | ... | Type99999NINE),我们想要确定它是否可以赋值给 SomeType。 回想一下,我们实际上没有一个交叉类型作为源类型,而是一个联合类型,看起来像是 (SomeType & Type1) | (SomeType & Type2) | ... | (SomeType & Type99999NINE)。 当检查一个联合类型是否可以赋值给目标类型时,我们必须检查联合类型的每个成员是否可以赋值给目标类型,这可能非常慢。

在 TypeScript 5.3 中,我们查看了我们能够隐藏的原始交叉类型形式。 当我们比较这些类型时,我们会快速检查目标类型是否存在于源交叉类型的任何组成部分中。

更多详情请参考PR

合并 tsserverlibrary.jstypescript.js

TypeScript 本身包含两个库文件:tsserverlibrary.jstypescript.js。 在 tsserverlibrary.js 中有一些仅在其中可用的 API(如 ProjectService API),对某些导入者可能很有用。 尽管如此,这两个是不同的捆绑包,有很多重叠的部分,在包中重复了一些代码。 更重要的是,由于自动导入或肌肉记忆的原因,要始终一致地使用其中一个可能是具有挑战性的。 意外加载两个模块太容易了,而且代码可能在 API 的不同实例上无法正常工作。 即使它可以工作,加载第二个捆绑包会增加资源使用量。

基于此,我们决定合并这两个文件。 typescript.js 现在包含了以前在 tsserverlibrary.js 中的内容,而 tsserverlibrary.js 现在只是重新导出 typescript.js。 在这个合并前后,我们看到了以下包大小的减小:

BeforeAfterDiffDiff (percent)
Packed6.90 MiB5.48 MiB-1.42 MiB-20.61%
Unpacked38.74 MiB30.41 MiB-8.33 MiB-21.50%
BeforeAfterDiffDiff (percent)
lib/tsserverlibrary.d.ts570.95 KiB865.00 B-570.10 KiB-99.85%
lib/tsserverlibrary.js8.57 MiB1012.00 B-8.57 MiB-99.99%
lib/typescript.d.ts396.27 KiB570.95 KiB+174.68 KiB+44.08%
lib/typescript.js7.95 MiB8.57 MiB+637.53 KiB+7.84%

换句话说,这意味着包大小减小了超过 20.5%。

更多详情请参考 PR

TypeScript 5.2

using 声明与显式资源管理

TypeScript 5.2 支持了 ECMAScript 即将引入的新功能 显式资源管理。 让我们探索一下引入该功能的一些动机,并理解这个功能给我们带来了什么。

在创建对象之后需要进行某种形式的“清理”是很常见的。例如,您可能需要关闭网络连接,删除临时文件,或者只是释放一些内存。 让我们来想象一个函数,它创建一个临时文件,对它进行多种操作的读写,然后关闭并删除它。

import * as fs from 'fs';

export function doSomeWork() {
  const path = '.some_temp_file';
  const file = fs.openSync(path, 'w+');

  // use file...

  // Close the file and delete it.
  fs.closeSync(file);
  fs.unlinkSync(path);
}

这看起来不错,但如果需要提前退出会发生什么?

export function doSomeWork() {
  const path = '.some_temp_file';
  const file = fs.openSync(path, 'w+');

  // use file...
  if (someCondition()) {
    // do some more work...

    // Close the file and delete it.
    fs.closeSync(file);
    fs.unlinkSync(path);
    return;
  }

  // Close the file and delete it.
  fs.closeSync(file);
  fs.unlinkSync(path);
}

我们可以看到存在重复的容易忘记的清理代码。 同时无法保证在代码抛出异常时,关闭和删除文件会被执行。 解决办法是用 try/finally 语句包裹整段代码。

export function doSomeWork() {
  const path = '.some_temp_file';
  const file = fs.openSync(path, 'w+');

  try {
    // use file...

    if (someCondition()) {
      // do some more work...
      return;
    }
  } finally {
    // Close the file and delete it.
    fs.closeSync(file);
    fs.unlinkSync(path);
  }
}

虽说这样写更加健壮,但是也为我们的代码增加了一些“噪音”。 如果我们在 finally 块中开始添加更多的清理逻辑,还可能遇到其他的自食其果的问题。 例如,异常可能会阻止其他资源的释放。 这些就是显式资源管理想要解决的问题。 该提案的关键思想是将资源释放(我们试图处理的清理工作)作为 JavaScript 中的一等概念来支持。

首先,增加了一个新的 symbol 名字为 Symbol.dispose,然后可以定义包含 Symbol.dispose 方法的对象。 为了方便,TypeScript 为此定义了一个新的全局类型 Disposable

class TempFile implements Disposable {
  #path: string;
  #handle: number;

  constructor(path: string) {
    this.#path = path;
    this.#handle = fs.openSync(path, 'w+');
  }

  // other methods

  [Symbol.dispose]() {
    // Close the file and delete it.
    fs.closeSync(this.#handle);
    fs.unlinkSync(this.#path);
  }
}

之后可以调用这些方法

export function doSomeWork() {
  const file = new TempFile('.some_temp_file');

  try {
    // ...
  } finally {
    file[Symbol.dispose]();
  }
}

将清理逻辑移动到 TempFile 本身没有带来多大的价值;仅仅是将清理的代码从 finally 提取到方法而已,你总是可以这样做。 但如果该方法有一个众所周知的名字那么 JavaScript 就可以基于此构造其它功能。

这将引出该功能的第一个亮点:using 声明! using 是一个新的关键字,支持声明新的不可变绑定,像 const 一样。 不同点是 using 声明的变量在即将离开其作用域时,它的 Symbol.dispose 方法会被调用!

因此,我们可以这样编写代码:

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}

看一下 - 没有 try / finally 代码块!至少,我们没有见到。 从功能上讲,这些正是 using 声明要帮我们做的事,但我们不必自己处理它。

你可能熟悉 C# 中的 using, Python 中的 with,Java 中的 try-with-resource 声明。 这些与 JavaScript 中的 using 关键字是相似的,都提供了一种明确的方式来“清理”对象,在它们即将离开作用域时。

using 声明在其所在的作用域的最后才执行清理工作,或在“提前返回”(如 return 语句或 throw 错误)之前执行清理工作。 释放的顺序是先入后出,像栈一样。

function loggy(id: string): Disposable {
    console.log(`Creating ${id}`);

    return {
        [Symbol.dispose]() {
            console.log(`Disposing ${id}`);
        }
    }
}

function func() {
    using a = loggy("a");
    using b = loggy("b");
    {
        using c = loggy("c");
        using d = loggy("d");
    }
    using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    using f = loggy("f");
}

func();
// Creating a
// Creating b
// Creating c
// Creating d
// Disposing d
// Disposing c
// Creating e
// Disposing e
// Disposing b
// Disposing a

using 声明对异常具有适应性;如果抛出了一个错误,那么在资源释放后会重新抛出错误。 另一方面,一个函数体可能正常执行,但是 Symbol.dispose 可能抛出异常。 这种情况下,异常会被重新抛出。

但如果释放之前的逻辑以及释放时的逻辑都抛出了异常会发生什么? 为处理这类情况引入了一个新的类型 SuppressedError,它是 Error 类型的子类型。 SuppressedError 类型的 suppressed 属性保存了上一个错误,同时 error 属性保存了最后抛出的错误。

class ErrorA extends Error {
    name = "ErrorA";
}
class ErrorB extends Error {
    name = "ErrorB";
}

function throwy(id: string) {
    return {
        [Symbol.dispose]() {
            throw new ErrorA(`Error from ${id}`);
        }
    };
}

function func() {
    using a = throwy("a");
    throw new ErrorB("oops!")
}

try {
    func();
}
catch (e: any) {
    console.log(e.name); // SuppressedError
    console.log(e.message); // An error was suppressed during disposal.

    console.log(e.error.name); // ErrorA
    console.log(e.error.message); // Error from a

    console.log(e.suppressed.name); // ErrorB
    console.log(e.suppressed.message); // oops!
}

你可能已经注意到了,在这些例子中使用的都是同步方法。 然而,很多资源释放的场景涉及到异步操作,我们需要等待它们完成才能进行后续的操作。

这就是为什么现在还有一个新的 Symbol.asyncDispose,它带来了另一个亮点 - await using 声明。 它与 using 声明相似,但关键是它查找需要 await 的资源。 它使用名为 Symbol.asyncDispose 的方法,尽管它们也可以操作在任何具有 Symbol.dispose 的对象上操作。 为了方便,TypeScript 引入了全局类型 AsyncDisposable 用来表示拥有异步 dispose 方法的对象。

async function doWork() {
    // Do fake work for half a second.
    await new Promise(resolve => setTimeout(resolve, 500));
}

function loggy(id: string): AsyncDisposable {
    console.log(`Constructing ${id}`);
    return {
        async [Symbol.asyncDispose]() {
            console.log(`Disposing (async) ${id}`);
            await doWork();
        },
    }
}

async function func() {
    await using a = loggy("a");
    await using b = loggy("b");
    {
        await using c = loggy("c");
        await using d = loggy("d");
    }
    await using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    await using f = loggy("f");
}

func();
// Constructing a
// Constructing b
// Constructing c
// Constructing d
// Disposing (async) d
// Disposing (async) c
// Constructing e
// Disposing (async) e
// Disposing (async) b
// Disposing (async) a

如果你期望其他人能够一致地执行清理逻辑,通过使用 DisposableAsyncDisposable 来定义类型可以使你的代码更易于使用。 实际上,存在许多现有的类型,它们拥有 dispose()close() 方法。 例如,Visual Studio Code APIs 定义了 自己的 Disposable 接口。 在浏览器和诸如 Node.js、Deno 和 Bun 等运行时中,API 也可以选择对已经具有清理方法(如文件句柄、连接等)的对象使用 Symbol.disposeSymbol.asyncDispose

现在也许对于库来说这听起来很不错,但对于你的场景来说可能有些过于复杂。如果你需要进行大量的临时清理,创建一个新类型可能会引入过度抽象和关于最佳实践的问题。 例如,再次以我们的 TempFile 示例为例。

class TempFile implements Disposable {
    #path: string;
    #handle: number;

    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }

    // other methods

    [Symbol.dispose]() {
        // Close the file and delete it.
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}

我们只是想记住调用两个函数,但这是最好的写法吗? 我们应该在构造函数中调用 openSync,创建一个 open() 方法,还是自己传递句柄? 我们是否应该为每个需要执行的操作公开一个方法,还是只将属性公开?

这就引出了这个特性的最后亮点:DisposableStackAsyncDisposableStack。 这些对象非常适用于一次性的清理工作,以及任意数量的清理工作。 DisposableStack 是一个对象,它具有多个方法用于跟踪 Disposable 对象,并且可以接受函数来执行任意的清理工作。 我们还可以将它们分配给 using 变量,因为它们也是 Disposable!所以下面是我们可以编写原始示例的方式。

function doSomeWork() {
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");

    using cleanup = new DisposableStack();
    cleanup.defer(() => {
        fs.closeSync(file);
        fs.unlinkSync(path);
    });

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }

    // ...
}

在这里,defer() 方法只需要一个回调函数,该回调函数将在 cleanup 释放后运行。 通常,在创建资源后应立即调用 defer(以及其他 DisposableStack 方法,如 useadopt)。 顾名思义,DisposableStack 以类似堆栈的方式处理它所跟踪的所有内容,按照先进后出的顺序进行处理,因此在创建值后立即进行 defer 处理有助于避免奇怪的依赖问题。 AsyncDisposableStack 的工作原理类似,但可以跟踪异步函数和 AsyncDisposable,并且本身也是 AsyncDisposable

在许多方面,defer 方法与 Go、Swift、Zig、Odin 等语言中的 defer 关键字类似,因此其使用约定应该相似。

由于这个特性非常新,大多数运行时环境不会原生支持它。要使用它,您需要为以下内容提供运行时的 polyfills:

  • Symbol.dispose
  • Symbol.asyncDispose
  • DisposableStack
  • AsyncDisposableStack
  • SuppressedError

然而,如果您只对使用 usingawait using 感兴趣,您只需要为内置的 symbol 提供 polyfill,通常以下简单的方法可适用于大多数情况:

Symbol.dispose ??= Symbol('Symbol.dispose');
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');

你还需要将编译 target 设置为 es2022 或以下,配置 lib"esnext""esnext.disposable"

{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.disposable", "dom"]
    }
}

更多详情请参考PR

Decorator Metadata

TypeScript 5.2 实现了 ECMAScript 即将引入的新功能 Decorator Metadata

这个功能的关键思想是使装饰器能够轻松地在它们所使用或嵌套的任何类上创建和使用元数据。

在任意的装饰器函数上,现在可以访问上下文对象的 metadata 属性。 metadata 属性是一个普通的对象。 由于 JavaScript 允许我们对其任意添加属性,它可以被用作可由每个装饰器更新的字典。 或者,由于每个 metadata 对象对于每个被装饰的部分来讲是等同的,它可以被用作 Map 的键。 当类的装饰器运行时,这个对象可以通过 Symbol.metadata 访问。

interface Context {
  name: string;
  metadata: Record;
}

function setMetadata(_target: any, context: Context) {
  context.metadata[context.name] = true;
}

class SomeClass {
  @setMetadata
  foo = 123;

  @setMetadata
  accessor bar = 'hello!';

  @setMetadata
  baz() {}
}

const ourMetadata = SomeClass[Symbol.metadata];

console.log(JSON.stringify(ourMetadata));
// { "bar": true, "baz": true, "foo": true }

它可以被应用在不同的场景中。 Metadata 信息可以附加在调试、序列化或者依赖注入的场景中。 由于每个被装饰的类都会生成 metadata 对象,框架可以选择用它们做为 key 来访问 MapWeakMap,或者跟踪它的属性。

例如,我们想通过装饰器来跟踪哪些属性和存取器是可以通过 Json.stringify 序列化的:

import { serialize, jsonify } from './serializer';

class Person {
  firstName: string;
  lastName: string;

  @serialize
  age: number;

  @serialize
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  toJSON() {
    return jsonify(this);
  }

  constructor(firstName: string, lastName: string, age: number) {
    // ...
  }
}

此处的意图是,只有 agefullName 可以被序列化,因为它们应用了 @serialize 装饰器。 我们定义了 toJSON 方法来做这件事,但它只是调用了 jsonfy,它会使用 @serialize 创建的 metadata

下面是 ./serialize.ts 可能的定义:

const serializables = Symbol();

type Context =
  | ClassAccessorDecoratorContext
  | ClassGetterDecoratorContext
  | ClassFieldDecoratorContext;

export function serialize(_target: any, context: Context): void {
  if (context.static || context.private) {
    throw new Error('Can only serialize public instance members.');
  }
  if (typeof context.name === 'symbol') {
    throw new Error('Cannot serialize symbol-named properties.');
  }

  const propNames = ((context.metadata[serializables] as
    | string[]
    | undefined) ??= []);
  propNames.push(context.name);
}

export function jsonify(instance: object): string {
  const metadata = instance.constructor[Symbol.metadata];
  const propNames = metadata?.[serializables] as string[] | undefined;
  if (!propNames) {
    throw new Error('No members marked with @serialize.');
  }

  const pairStrings = propNames.map(key => {
    const strKey = JSON.stringify(key);
    const strValue = JSON.stringify((instance as any)[key]);
    return `${strKey}: ${strValue}`;
  });

  return `{ ${pairStrings.join(', ')} }`;
}

该方法有一个局部 symbol 名字为 serializables 用于保存和获取使用 @serializable 标记的属性。 当每次调用 @serializable 时,它都会在 metadata 上保存这些属性名。 当 jsonfy 被调用时,从 metadata 上获取属性列表,之后从实例上获取实际值,最后序列化名和值。

使用 symbol 意味着该数据可以被他人访问。 另一选择是使用 WeakMap 并用该 metadata 对象做为键。 这样可以保持数据的私密性,并且在这种情况下使用更少的类型断言,但其他方面类似。

const serializables = new WeakMap();

type Context =
  | ClassAccessorDecoratorContext
  | ClassGetterDecoratorContext
  | ClassFieldDecoratorContext;

export function serialize(_target: any, context: Context): void {
  if (context.static || context.private) {
    throw new Error('Can only serialize public instance members.');
  }
  if (typeof context.name !== 'string') {
    throw new Error('Can only serialize string properties.');
  }

  let propNames = serializables.get(context.metadata);
  if (propNames === undefined) {
    serializables.set(context.metadata, (propNames = []));
  }
  propNames.push(context.name);
}

export function jsonify(instance: object): string {
  const metadata = instance.constructor[Symbol.metadata];
  const propNames = metadata && serializables.get(metadata);
  if (!propNames) {
    throw new Error('No members marked with @serialize.');
  }
  const pairStrings = propNames.map(key => {
    const strKey = JSON.stringify(key);
    const strValue = JSON.stringify((instance as any)[key]);
    return `${strKey}: ${strValue}`;
  });

  return `{ ${pairStrings.join(', ')} }`;
}

注意,这里的实现没有考虑子类和继承。 留给读者作为练习。

由于该功能比较新,大多数运行时都没实现它。 如果想要使用,则需要使用 Symbol.metadatapolyfill。 例如像下面这样就可以适用大部分场景:

Symbol.metadata ??= Symbol('Symbol.metadata');

你还需要将编译 target 设为 es2022 或以下,配置 lib"esnext" 或者 "esnext.decorators"

{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.decorators", "dom"]
    }
}

感谢 Oleksandr Tarasiuk贡献

命名的和匿名的元组元素

元组类型已经支持了为每个元素定义可选的标签和命名。

type Pair = [first: T, second: T];

这些标签不改变功能 - 它们只是用于增强可读性和工具支持。

然而,TypeScript 之前有个限制是不允许混用有标签和无标签的元素。 换句话说,要么所有元素都没有标签,要么所有元素都有标签。

// ✅ fine - no labels
type Pair1 = [T, T];

// ✅ fine - all fully labeled
type Pair2 = [first: T, second: T];

// ❌ previously an error
type Pair3 = [first: T, T];
//                         ~
// Tuple members must all have names
// or all not have names.

如果是剩余元素就比较烦人了,我们必须要添加标签 rest 或者 tail

// ❌ previously an error
type TwoOrMore_A = [first: T, second: T, ...T[]];
//                                          ~~~~~~
// Tuple members must all have names
// or all not have names.

// ✅
type TwoOrMore_B = [first: T, second: T, rest: ...T[]];

这也意味着这个限制必须在类型系统内部进行强制执行,这意味着 TypeScript 将失去标签。

type HasLabels = [a: string, b: string];
type HasNoLabels = [number, number];
type Merged = [...HasNoLabels, ...HasLabels];
//   ^ [number, number, string, string]
//
//     'a' and 'b' were lost in 'Merged'

在 TypeScript 5.2 中,对元组标签的全有或全无限制已经被取消。 而且现在可以在展开的元组中保留标签。

感谢 Josh GoldbergMateusz Burzyński 的贡献。

更容易地使用联合数组上的方法

在之前版本的 TypeScript 中,在联合数组上调用方法可能很痛苦。

declare let array: string[] | number[];

array.filter(x => !!x);
//    ~~~~~~ error!
// This expression is not callable.
//   Each member of the union type '...' has signatures,
//   but none of those signatures are compatible
//   with each other.

此例中,TypeScript 会检查是否每个版本的 filter 都与 string[]number[] 兼容。 在没有一个连贯的策略的情况下,TypeScript 会束手无策地说:“我无法使其工作”。

在 TypeScript 5.2 里,在放弃之前,联合数组会被特殊对待。 使用每个元素类型构造一个新数组,然后在其上调用方法。

对于上例来说,string[] | number[] 被转换为 (string | number)[](或者是 Array<string | number>),然后在该类型上调用 filter。 有一个注意事项,filter 会产生 Array<string | number> 而不是 string[] | number[]; 但对于新产生的值,出现“出错”的风险较小。

这意味着在以前不能使用的情况下,许多方法如 filterfindsomeeveryreduce 都可以在数组的联合类型上调用。

更多详情请参考PR

拷贝的数组方法

TypeScript 5.2 支持了 ECMAScript 提案 Change Array by Copy

JavaScript 中的数组有很多有用的方法如 sort()splice(),以及 reverse(),这些方法在数组中原地修改元素。 通常,我们想创建一个新数组,还想影响原来的数组。 为达到此目的,你可以使用 slice() 或者展开数组(例如 [...myArray])获取一份拷贝,然后再执行操作。 例如,你可以用 myArray.slice().reverse() 来获取反转的数组的拷贝。

还有一个典型的例子 - 创建一份拷贝,但是修改其中的一个元素。 有许多方法可以实现这一点,但最明显的方法要么是由多个语句组成的...

const copy = myArray.slice();
copy[someIndex] = updatedValue;
doSomething(copy);

要么意图不明显...

doSomething(
  myArray.map((value, index) => (index === someIndex ? updatedValue : value))
);

所有这些对于如此常见的操作来说都很繁琐。 这就是为什么 JavaScript 现在有了 4 个新的方法,执行相同的操作,但不影响原始数据:toSortedtoSplicedtoReversedwith。 前三个方法执行与它们的变异版本相同的操作,但返回一个新的数组。 with 也返回一个新的数组,但其中一个元素被更新(如上所述)。

修改拷贝
myArray.reverse()myArray.toReversed()
myArray.sort((a, b) => ...)myArray.toSorted((a, b) => ...)
myArray.splice(start, deleteCount, ...items)myArray.toSpliced(start, deleteCount, ...items)
myArray[index] = updatedValuemyArray.with(index, updatedValue)

请注意,复制方法始终创建一个新的数组,而修改操作则不一致。

这些方法不仅存在于普通数组上 - 它们还存在于 TypedArray 上,例如 Int32ArrayUint8Array,等。

感谢 Carter SnookPR

symbol 用于 WeakMapWeakSet 的键

现在可以将 symbol 用于 WeakMapWeakSet 的键,它也是 ECMAScript 的新功能

const myWeakMap = new WeakMap();

const key = Symbol();
const someObject = { /*...*/ };

// Works! ✅
myWeakMap.set(key, someObject);
myWeakMap.has(key);

这个更新是由 Leo Elmecker-Plakolm 代表 Bloomberg 提供的。我们想向他们表示感谢!

类型导入路径里使用 TypeScript 实现文件扩展名

TypeScript 支持在类型导入路径里使用声明文件扩展名和实现文件扩展名,不论是否启用了 allowImportingTsExtensions

也意味着你现在可以编写 import type 语句并使用 .ts, .mts, .cts 以及 .tsx 文件扩展。

import type { JustAType } from "./justTypes.ts";

export function f(param: JustAType) {
    // ...
}

这也意味着,import() 类型(用在 TypeScript 和 JavaScript 的 JSDoc 中) 也可以使用这些扩展名。

/**
 * @param {import("./justTypes.ts").JustAType} param
 */
export function f(param) {
    // ...
}

更多详情请查看 PR

对象成员的逗号补全

在给对象添加新属性时很容易忘记添加逗号。 在之前,如果你忘了写逗号并且请求自动补全,TypeScript 会给出差的不相关的补全结果。

TypeScript 5.2 现在在您缺少逗号时会优雅地提供对象成员的自动补全。 但为了避免语法错误的出现,它还会自动插入缺失的逗号。

更多详情请查看 PR

内联变量重构

TypeScript 5.2 现在具有一种重构方法,可以将变量的内容内联到所有使用位置。

使用“内联变量”重构将消除变量并将所有变量的使用替换为其初始化值。 请注意,这可能会导致初始化程序的副作用在不同的时间运行,并且运行的次数与变量的使用次数相同。

更多详情请查看 PR

可点击的内嵌参数提示

内嵌提示可以让我们一目了然地获取信息,即使它在我们的代码中不存在 —— 比如参数名称、推断类型等等。 在 TypeScript 5.2 中,我们开始使得与内嵌提示进行交互成为可能。 例如,在 Visual Studio Code Insiders 中,您现在可以点击内联提示以跳转到参数的定义处。

更多详情请查看 PR

优化进行中的类型兼容性检查

由于 TypeScript 采用的是结构化的类型系统,通常需要比较类型成员; 然而,递归类型会造成一些问题。例如:

interface A {
    value: A;
    other: string;
}

interface B {
    value: B;
    other: number;
}

在检查 A 是否与 B 类型兼容时,TypeScript 会检查 ABvalue 的类型是否兼容。 此时,类型系统需要停止进一步检查并继续检查其他成员。 为此,类型系统必须跟踪两个类型是否已经相关联。

此前,TypeScript 已经保存了配对类型的栈,并迭代检查类型是否已经关联。 当这个堆栈很浅时,这不是一个问题;但当堆栈不是很浅时,那就是个问题了。

在 TypeScript 5.2 中,一个简单的 Set 就能跟踪这些信息。 在使用了 drizzle 库的测试报告中,这项改动减少了超过 33% 的时间花费!

Benchmark 1: old
  Time (mean ± σ):      3.115 s ±  0.067 s    [User: 4.403 s, System: 0.124 s]
  Range (min … max):    3.018 s …  3.196 s    10 runs

Benchmark 2: new
  Time (mean ± σ):      2.072 s ±  0.050 s    [User: 3.355 s, System: 0.135 s]
  Range (min … max):    1.985 s …  2.150 s    10 runs

Summary
  'new' ran
    1.50 ± 0.05 times faster than 'old'

更多详情请查看 PR

TypeScript 5.1

更易用的隐式返回 undefined 的函数

JavaScript 中,如果一个函数运行结束时没有遇到 return 语句,它会返回 undefined 值。

function foo() {
  // no return
}

// x = undefined
let x = foo();

然而,在之前版本的 TypeScript 中,只有返回值类型为 voidany 的函数可以不带 return 语句。 这意味着,就算明知函数返回 undefined,你也必须包含 return 语句。

//  fine - we inferred that 'f1' returns 'void'
function f1() {
  // no returns
}

//  fine - 'void' doesn't need a return statement
function f2(): void {
  // no returns
}

//  fine - 'any' doesn't need a return statement
function f3(): any {
  // no returns
}

//  error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
function f4(): undefined {
  // no returns
}

如果某些 API 期望函数返回 undefined 值,这可能会让人感到痛苦 —— 你需要至少有一个显式的返回 undefined 语句,或者一个带有显式注释的 return 语句。

declare function takesFunction(f: () => undefined): undefined;

//  error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(() => {
  // no returns
});

//  error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
takesFunction((): undefined => {
  // no returns
});

//  error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(() => {
  return;
});

//  works
takesFunction(() => {
  return undefined;
});

//  works
takesFunction((): undefined => {
  return;
});

这种行为非常令人沮丧和困惑,尤其是在调用自己无法控制的函数时。 理解推断 voidundefined 之间的相互作用,以及一个返回 undefined 的函数是否需要 return 语句等等,似乎会分散注意力。

首先,TypeScript 5.1 允许返回 undefined 的函数不包含返回语句。

//  Works in TypeScript 5.1!
function f4(): undefined {
  // no returns
}

//  Works in TypeScript 5.1!
takesFunction((): undefined => {
  // no returns
});

其次,如果一个函数没有返回表达式,并且被传递给期望返回 undefined 值的函数的地方,TypeScript 会推断该函数的返回类型为 undefined

//  Works in TypeScript 5.1!
takesFunction(function f() {
  //                 ^ return type is undefined
  // no returns
});

//  Works in TypeScript 5.1!
takesFunction(function f() {
  //                 ^ return type is undefined

  return;
});

为了解决另一个类似的痛点,在 TypeScript 的 --noImplicitReturns 选项下,只返回 undefined 的函数现在有了类似于 void 的例外情况,在这种情况下,并不是每个代码路径都必须以显式的返回语句结束。

//  Works in TypeScript 5.1 under '--noImplicitReturns'!
function f(): undefined {
  if (Math.random()) {
    // do some stuff...
    return;
  }
}

更多详情请参考 IssuePR

不相关的存取器类型

TypeScript 4.3 支持将成对的 getset 定义为不同的类型。

interface Serializer {
  set value(v: string | number | boolean);
  get value(): string;
}

declare let box: Serializer;

// Allows writing a 'boolean'
box.value = true;

// Comes out as a 'string'
console.log(box.value.toUpperCase());

最初,我们要求 get 的类型是 set 类型的子类型。这意味着:

box.value = box.value;

永远是合法的。

然而,大量现存的和提议的 API 带有毫无关联的 getset 类型。 例如,考虑一个常见的情况 - DOM 中的 style 属性和 CSSStyleRule API。 每条样式规则都有一个 style 属性,它是一个 CSSStyleDeclaration; 然而,如果你尝试给该属性写值,它仅支持字符串。

TypeScript 5.1 现在允许为 getset 访问器属性指定完全不相关的类型,前提是它们具有显式的类型注解。 虽然这个版本的 TypeScript 还没有改变这些内置接口的类型,但 CSSStyleRule 现在可以按以下方式定义:

interface CSSStyleRule {
  // ...

  /** Always reads as a `CSSStyleDeclaration` */
  get style(): CSSStyleDeclaration;

  /** Can only write a `string` here. */
  set style(newValue: string);

  // ...
}

这也允许其他模式,比如要求 set 访问器只接受“有效”的数据,但指定 get 访问器可以返回 undefined,如果某些基础状态还没有被初始化。

class SafeBox {
  #value: string | undefined;

  // Only accepts strings!
  set value(newValue: string) {}

  // Must check for 'undefined'!
  get value(): string | undefined {
    return this.#value;
  }
}

实际上,这与在 --exactOptionalProperties 选项下可选属性的检查方式类似。

更多详情请参考 PR

解耦 JSX 元素和 JSX 标签类型之间的类型检查

TypeScript 在 JSX 方面的一个痛点是对每个 JSX 元素标签的类型要求。 这个 TypeScript 版本使得 JSX 库更准确地描述了 JSX 组件可以返回的内容。 对于许多人来说,这具体意味着可以在 React 中使用异步服务器组件

做为背景知识,JSX 元素是下列其一:

// A self-closing JSX tag
<Foo />

// A regular element with an opening/closing tag
<Bar></Bar>

在类型检查 <Foo /><Bar></Bar> 时,TypeScript 总是查找名为 JSX 的命名空间,并且获取名为 Element 的类型。 换句话说,它查找 JSX.Element

但是为了检查 FooBar 是否是有效的标签名,TypeScript 大致上只需获取由 FooBar 返回或构造的类型,并检查其与 JSX.Element(或另一种叫做 JSX.ElementClass 的类型,如果该类型可构造)的兼容性。

这里的限制意味着如果组件返回或 “render” 比 JSX.Element 更宽泛的类型,则无法使用组件。 例如,一个 JSX 库可能会允许组件返回 strings 或 Promises。

作为一个更具体的例子,未来版本的 React 已经提出了对返回 Promise 的组件的有限支持,但是现有版本的 TypeScript 无法表达这一点,除非有人大幅放宽 JSX.Element 类型。

import * as React from 'react';

async function Foo() {
  return <div></div>;
}

let element = <Foo />;
//             ~~~
// 'Foo' cannot be used as a JSX component.
//   Its return type 'Promise<Element>' is not a valid JSX element.

为了给 library 提供一种表达这种情况的方法,TypeScript 5.1 现在查找一个名为 JSX.ElementType 的类型。ElementType 精确地指定了在 JSX 元素中作为标签使用的内容。 因此现在可以像如下这样定义:

namespace JSX {
    export type ElementType =
        // All the valid lowercase tags
        keyof IntrinsicAttributes
        // Function components
        (props: any) => Element
        // Class components
        new (props: any) => ElementClass;

    export interface IntrinsictAttributes extends /*...*/ {}
    export type Element = /*...*/;
    export type ClassElement = /*...*/;
}

感谢 Sebastian SilbermannPR

带有命名空间的 JSX 属性

TypeScript 支持在 JSX 里使用带有命名空间的属性。

import * as React from "react";

// Both of these are equivalent:
const x = <Foo a:b="hello" />;
const y = <Foo a : b="hello" />;

interface FooProps {
    "a:b": string;
}

function Foo(props: FooProps) {
    return <div>{props["a:b"]}</div>;
}

当名字的第一段是小写名称时,在 JSX.IntrinsicAttributes 上查找带命名空间的标记名是类似的。

// In some library's code or in an augmentation of that library:
namespace JSX {
  interface IntrinsicElements {
    ['a:b']: { prop: string };
  }
}

// In our code:
let x = <a:b prop="hello!" />;

感谢 Oleksandr TarasiukPR

模块解析时考虑 typeRoots

当 TypeScript 的模块解析策略无法解析一个路径时,它现在会相对于 typeRoots 继续解析。

更多详情请参考 PR

在 JSX 标签上链接光标

TypeScript 现在支持 链接编辑 JSX 标签名。 链接编辑(有时称作“光标镜像”)允许编辑器同时自动编辑多个位置。

这个新特性在 TypeScript 和 JavaScript 里都可用,并且可以在 Visual Studio Code Insiders 版本中启用。 在 Visual Studio Code 里,你既可以用设置界面的 Editor: Linked Editing 配置:

也可以用 JSON 配置文件中的 editor.linkedEditing

{
  // ...
  "editor.linkedEditing": true
}

这个功能也将在 Visual Studio 17.7 Preview 1 中得到支持。

@param JSDoc 标记的代码片段自动补全

现在,在 TypeScript 和 JavaScript 文件中输入 @param 标签时,TypeScript 提供代码片段自动补全。 这可以帮助在为代码编写文档和添加 JSDoc 类型时,减少打字和文本跳转次数。

更多详情请参考 PR

优化

避免非必要的类型初始化

TypeScript 5.1 现在避免在已知不包含对外部类型参数的引用的对象类型中执行类型实例化。 这有可能减少许多不必要的计算,并将 material-ui 的文档目录的类型检查时间缩短了 50% 以上。

更多详情请参考 PR

联合字面量的反面情况检查

当检查源类型是否是联合类型的一部分时,TypeScript 首先使用该源类型的内部类型标识符进行快速查找。 如果查找失败,则 TypeScript 会检查与联合类型中的每个类型的兼容性。

当将字面量类型与纯字面量类型的联合类型进行关联时,TypeScript 现在可以避免针对联合中的每个其他类型进行完整遍历。 这个假设是安全的,因为 TypeScript 总是将字面量类型内部化/缓存 —— 虽然有一些与“全新”字面量类型相关的边缘情况需要处理。

这个优化可以减少问题代码的类型检查时间从 45 秒到 0.4 秒。

减少在解析 JSDoc 时的扫描函数调用

在旧版本的 TypeScript 中解析 JSDoc 注释时,它们会使用扫描器/标记化程序将注释分解为细粒度的标记,然后将内容拼回到一起。 这对于规范化注释文本可能是有帮助的,使多个空格只折叠成一个; 但这样做会极大地增加“对话”量,意味着解析器和扫描器会非常频繁地来回跳跃,从而增加了 JSDoc 解析的开销。

TypeScript 5.1 已经移动了更多的逻辑来分解 JSDoc 注释到扫描器/标记化程序中。 现在,扫描器直接将更大的内容块返回给解析器,以便根据需要进行处理。

这些更改将几个大约 10Mb 的大部分为散文评论的 JavaScript 文件的解析时间减少了约一半。 对于一个更现实的例子,我们的性能套件对 xstate 的快照减少了约 300 毫秒的解析时间,使其更快地加载和分析。

TypeScript 5.0

装饰器 Decorators

装饰器是即将到来的 ECMAScript 特性,它允许我们定制可重用的类以及类成员。

考虑如下的代码:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person('Ron');
p.greet();

这里的 greet 很简单,但我们假设它很复杂 - 例如包含异步的逻辑,是递归的,具有副作用等。 不管你把它想像成多么混乱复杂,现在我们想插入一些 console.log 语句来调试 greet

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log('LOG: Entering method.');

    console.log(`Hello, my name is ${this.name}.`);

    console.log('LOG: Exiting method.');
  }
}

这个做法太常见了。 如果有种办法能给每一个类方法都添加打印功能就太好了!

这就是装饰器的用武之地。 让我们编写一个函数 loggedMethod

function loggedMethod(originalMethod: any, _context: any) {
  function replacementMethod(this: any, ...args: any[]) {
    console.log('LOG: Entering method.');
    const result = originalMethod.call(this, ...args);
    console.log('LOG: Exiting method.');
    return result;
  }

  return replacementMethod;
}

"这些 any 是怎么回事?都啥啊?"

先别急 - 这里我们是想简化一下问题,将注意力集中在函数的功能上。 注意一下 loggedMethod 接收原方法(originalMethod)作为参数并返回一个函数:

  1. 打印 "Entering…" 消息
  2. this 值以及所有的参数传递给原方法
  3. 打印 "Exiting..." 消息,并且
  4. 返回原方法的返回值。

现在可以使用 loggedMethod装饰 greet 方法:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @loggedMethod
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person('Ron');
p.greet();

// 输出:
//
//   LOG: Entering method.
//   Hello, my name is Ron.
//   LOG: Exiting method.

我们刚刚在 greet 上使用了 loggedMethod 装饰器 - 注意一下写法 @loggedMethod。 这样做之后,loggedMethod 被调用时会传入被装饰的目标 target 以及一个上下文对象 context object 作为参数。 因为 loggedMethod 返回了一个新函数,因此这个新函数会替换掉 greet 的原始定义。

loggedMethod 的定义中带有第二个参数。 它就是上下文对象 context object,包含了一些有关于装饰器声明细节的有用信息 - 例如是否为 #private 成员,或者 static,或者方法的名称。 让我们重写 loggedMethod 来使用这些信息,并且打印出被装饰的方法的名字。

function loggedMethod(
  originalMethod: any,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);

  function replacementMethod(this: any, ...args: any[]) {
    console.log(`LOG: Entering method '${methodName}'.`);
    const result = originalMethod.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`);
    return result;
  }

  return replacementMethod;
}

我们使用了上下文参数。 TypeScript 提供了名为 ClassMethodDecoratorContext 的类型用于描述装饰器方法接收的上下文对象。

除了元数据外,上下文对象中还提供了一个有用的函数 addInitializer。 它提供了一种方式来 hook 到构造函数的起始位置。

例如在 JavaScript 中,下面的情形很常见:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;

    this.greet = this.greet.bind(this);
  }

  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

或者,greet 可以被声明为使用箭头函数初始化的属性。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet = () => {
    console.log(`Hello, my name is ${this.name}.`);
  };
}

这类代码的目的是确保 this 值不会被重新绑定,当 greet 被独立地调用或者在用作回调函数时。

const greet = new Person('Ron').greet;

// 我们不希望下面的调用失败
greet();

我们可以定义一个装饰器来利用 addInitializer 在构造函数里调用 bind

function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = context.name;
  if (context.private) {
    throw new Error(
      `'bound' cannot decorate private properties like ${methodName as string}.`
    );
  }
  context.addInitializer(function () {
    this[methodName] = this[methodName].bind(this);
  });
}

bound 没有返回值 - 因此当它装饰一个方法时,不会影响原先的方法。 但是,它会在字段被初始化前添加一些逻辑。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @bound
  @loggedMethod
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person('Ron');
const greet = p.greet;

// Works!
greet();

我们将两个装饰器叠在了一起 - @bound@loggedMethod。 这些装饰器以“相反的”顺序执行。 也就是说,@loggedMethod 装饰原始方法 greet@bound 装饰的是 @loggedMethod 的结果。 此例中,这不太重要 - 但如果你的装饰器带有副作用或者期望特定的顺序,那就不一样了。

值得注意的是:如果你在乎代码样式,也可以将装饰器放在同一行上。

@bound @loggedMethod greet() {
  console.log(`Hello, my name is ${this.name}.`);
}

可能不太明显的一点是,你甚至可以定义一个返回装饰器函数的函数。 这样我们可以在一定程序上定制最终的装饰器。 我们可以让 loggedMethod 返回一个装饰器并且定制如何打印消息。

function loggedMethod(headMessage = 'LOG:') {
  return function actualDecorator(
    originalMethod: any,
    context: ClassMethodDecoratorContext
  ) {
    const methodName = String(context.name);

    function replacementMethod(this: any, ...args: any[]) {
      console.log(`${headMessage} Entering method '${methodName}'.`);
      const result = originalMethod.call(this, ...args);
      console.log(`${headMessage} Exiting method '${methodName}'.`);
      return result;
    }

    return replacementMethod;
  };
}

这样做之后,在使用 loggedMethod 装饰器之前需要先调用它。 接下来就可以传入任意字符串作为打印消息的前缀。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @loggedMethod('')
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const p = new Person('Ron');
p.greet();

// Output:
//
//    Entering method 'greet'.
//   Hello, my name is Ron.
//    Exiting method 'greet'.

装饰器不仅可以用在方法上! 它们也可以被用在属性/字段,存取器(getter/setter)以及自动存取器。 甚至,类本身也可以被装饰,用于处理子类化和注册。

想深入了解装饰器,可以阅读 Axel Rauschmayer 的文章

更多详情请参考 PR

与旧的实验性的装饰器的差异

如果你有一定的 TypeScript 经验,你会发现 TypeScript 多年前就已经支持了“实验性的”装饰器特性。 虽然实验性的装饰器非常地好用,但是它实现的是旧版本的装饰器规范,并且总是需要启用 --experimentalDecorators 编译器选项。 若没有启用它并且使用了装饰器,TypeScript 会报错。

在未来的一段时间内,--experimentalDecorators 依然会存在; 然而,如果不使用该标记,在新代码中装饰器语法也是合法的。 在 --experimentalDecorators 之外,它们的类型检查和代码生成方式也不同。 类型检查和代码生成规则存在巨大差异,以至于虽然装饰器可以被定义为同时支持新、旧装饰器的行为,但任何现有的装饰器函数都不太可能这样做。

新的装饰器提案与 --emitDecoratorMetadata 的实现不兼容,并且不支持在参数上使用装饰器。 未来的 ECMAScript 提案可能会弥补这个差距。

最后要注意的是:除了可以在 export 关键字之前使用装饰器,还可以在 export 或者 export default 之后使用。 但是不允许混合使用两种风格。

//  allowed
@register
export default class Foo {
  // ...
}

//  also allowed
export default
@register
class Bar {
  // ...
}

//  error - before *and* after is not allowed
@before
@after
export class Bar {
  // ...
}

编写强类型的装饰器

上面的例子 loggedMethodbound 是故意写的简单并且忽略了大量和类型有关的细节。

为装饰器添加类型可能会很复杂。 例如,强类型的 loggedMethod 可能像下面这样:

function loggedMethod<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<
    This,
    (this: This, ...args: Args) => Return
  >
) {
  const methodName = String(context.name);

  function replacementMethod(this: This, ...args: Args): Return {
    console.log(`LOG: Entering method '${methodName}'.`);
    const result = target.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`);
    return result;
  }

  return replacementMethod;
}

我们必须分别给原方法的 this、形式参数和返回值添加类型,上面使用了类型参数 ThisArgs 以及 Return。 装饰器函数到底有多复杂取决于你要确保什么。 但要记住,装饰器被使用的次数远多于被编写的次数,因此强类型的版本是通常希望得到的 - 但我们需要在可读性之间做出取舍,因此要尽量保持简洁。

未来会有更多关于如何编写装饰器的文档 - 但是这篇文章详细介绍了装饰器的工作方式。

const 类型参数

在推断对象类型时,TypeScript 通常会选择一个通用类型。 例如,下例中 names 的推断类型为 string[]

type HasNames = { readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T['names'] {
  return arg.names;
}

// Inferred type: string[]
const names = getNamesExactly({ names: ['Alice', 'Bob', 'Eve'] });

这样做的目的通常是为了允许后面可以进行修改。

然而,根据 getNamesExactly 的具体功能和预期使用方式,通常情况下需要更加具体的类型。

直到现在,API 作者们通常不得不在一些位置上添加 as const 来达到预期的类型推断目的:

// The type we wanted:
//    readonly ["Alice", "Bob", "Eve"]
// The type we got:
//    string[]
const names1 = getNamesExactly({ names: ['Alice', 'Bob', 'Eve'] });

// Correctly gets what we wanted:
//    readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ['Alice', 'Bob', 'Eve'] } as const);

这样做既繁琐又容易忘。 在 TypeScript 5.0 里,你可以为类型参数声明添加 const 修饰符, 这使得 const 形式的类型推断成为默认行为:

type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T['names'] {
  //                       ^^^^^
  return arg.names;
}

// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ['Alice', 'Bob', 'Eve'] });

注意,const 修饰符不会拒绝可修改的值,并且不需要不可变约束。 使用可变类型约束可能会产生令人惊讶的结果。

declare function fnBad<const T extends string[]>(args: T): void;

// 'T' is still 'string[]' since 'readonly ["a", "b", "c"]' is not assignable to 'string[]'
fnBad(['a', 'b', 'c']);

这里,T 的候选推断类型为 readonly ["a", "b", "c"],但是 readonly 只读数组不能用在需要可变数组的地方。 这种情况下,类型推断会回退到类型约束,将数组视为 string[] 类型,因此函数调用仍然会成功。

这个函数更好的定义是使用 readonly string[]

declare function fnGood<const T extends readonly string[]>(args: T): void;

// T is readonly ["a", "b", "c"]
fnGood(['a', 'b', 'c']);

要注意 const 修饰符只影响在函数调用中直接写出的对象、数组和基本表达式的类型推断, 因此,那些无法(或不会)使用 as const 进行修饰的参数在行为上不会有任何变化:

declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ['a', 'b', 'c'];

// 'T' is still 'string[]'-- the 'const' modifier has no effect here
fnGood(arr);

更多详情请参考 PRPRPR

extends 支持多个配置文件

在管理多个项目时,拥有一个“基础”配置文件,其他 tsconfig.json 文件可以继承它,这会非常有帮助。 这就是为什么 TypeScript 支持使用 extends 字段来从 compilerOptions 中复制字段的原因。

// packages/front-end/src/tsconfig.json
{
  "extends": "../../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "../lib"
    // ...
  }
}

然而,有时您可能想要从多个配置文件中进行继承。 例如,假设您正在使用一个在 npm 上发布的 TypeScript 基础配置文件。 如果您希望自己所有的项目也使用 npm 上的 @tsconfig/strictest 包中的选项,那么有一个简单的解决方案:让 tsconfig.base.json@tsconfig/strictest 进行扩展:

// tsconfig.base.json
{
  "extends": "@tsconfig/strictest/tsconfig.json",
  "compilerOptions": {
    // ...
  }
}

这在某种程度上是有效的。 如果您的某些工程不想使用 @tsconfig/strictest,那么必须手动禁用这些选项,或者创建一个不继承于 @tsconfig/strictesttsconfig.base.json

为了提高灵活性,TypeScript 5.0 允许 extends 字段指定多个值。 例如,有如下的配置文件:

{
  "extends": ["a", "b", "c"],
  "compilerOptions": {
    // ...
  }
}

这样写就如同是直接继承 c,而 c 继承于 bb 继承于 a。 如果出现冲突,后来者会被采纳。

在下面的例子中,在最终的 tsconfig.jsonstrictNullChecksnoImplicitAny 会被启用。

// tsconfig1.json
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}

// tsconfig2.json
{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

// tsconfig.json
{
    "extends": ["./tsconfig1.json", "./tsconfig2.json"],
    "files": ["./index.ts"]
}

另一个例子,我们可以这样改写最初的示例:

// packages/front-end/src/tsconfig.json
{
  "extends": [
    "@tsconfig/strictest/tsconfig.json",
    "../../../tsconfig.base.json"
  ],
  "compilerOptions": {
    "outDir": "../lib"
    // ...
  }
}

更多详情请参考:PR

所有的 enum 均为联合 enum

在最初 TypeScript 引入枚举类型时,它们只不过是一组同类型的数值常量。

enum E {
  Foo = 10,
  Bar = 20,
}

E.FooE.Bar 唯一特殊的地方在于它们可以赋值给任何期望类型为 E 的地方。 除此之外,它们基本上等同于 number 类型。

function takeValue(e: E) {}

takeValue(E.Foo); // works
takeValue(123); // error!

直到 TypeScript 2.0 引入了枚举字面量类型,枚举才变得更为特殊。 枚举字面量类型为每个枚举成员提供了其自己的类型,并将枚举本身转换为每个成员类型的联合类型。 它们还允许我们仅引用枚举中的一部分类型,并细化掉那些类型。

// Color is like a union of Red | Orange | Yellow | Green | Blue | Violet
enum Color {
    Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet
}

// Each enum member has its own type that we can refer to!
type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
    // Narrowing literal types can catch bugs.
    // TypeScript will error here because
    // we'll end up comparing 'Color.Red' to 'Color.Green'.
    // We meant to use ||, but accidentally wrote &&.
    return c === Color.Red && c === Color.Green && c === Color.Blue;
}

为每个枚举成员提供其自己的类型的一个问题是,这些类型在某种程度上与成员的实际值相关联。 在某些情况下,无法计算该值 - 例如,枚举成员可能由函数调用初始化。

enum E {
  Blah = Math.random(),
}

每当 TypeScript 遇到这些问题时,它会悄悄地退而使用旧的枚举策略。 这意味着放弃所有联合类型和字面量类型的优势。

TypeScript 5.0 通过为每个计算成员创建唯一类型,成功将所有枚举转换为联合枚举。 这意味着现在所有枚举都可以被细化,并且每个枚举成员都有其自己的类型。

更多详情请参考 PR

--moduleResolution bundler

TypeScript 4.7 支持将 --module--moduleResolution 选项设置为 node16nodenext。 这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则; 然而,这种模式存在许多其他工具实际上并不强制执行的限制。

例如,在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名。

// entry.mjs
import * as utils from './utils'; //  wrong - we need to include the file extension.

import * as utils from './utils.mjs'; //  works

对于 Node.js 和浏览器来说,这样做有一些原因 - 它可以加快文件查找速度,并且对于简单的文件服务器效果更好。 但是对于许多使用打包工具的开发人员来说,node16 / nodenext 设置很麻烦, 因为打包工具中没有这么多限制。 在某些方面,node 解析模式对于任何使用打包工具的人来说是更好的。

但在某些方面,原始的 node 解析模式已经过时了。 大多数现代打包工具在 Node.js 中使用 ECMAScript 模块和 CommonJS 查找规则的融合。 例如,像在 CommonJS 中一样,无扩展名的导入也可以正常工作,但是在查找包的导出条件时,它们将首选像在 ECMAScript 文件中一样的 import 条件。

为了模拟打包工具的工作方式,TypeScript 现在引入了一种新策略:--moduleResolution bundler

{
  "compilerOptions": {
    "target": "esnext",
    "moduleResolution": "bundler"
  }
}

如果你使用如 Vite, esbuild, swc, Webpack, parcel 等现代打包工具,它们实现了混合的查找策略,新的 bundler 选项是更好的选择。

另一方面,如果您正在编写一个要发布到 npm 的代码库,那么使用 bundler 选项可能会隐藏影响未使用打包工具用户的兼容性问题。 因此,在这些情况下,使用 node16nodenext 解析选项可能是更好的选择。

更多详情请参考 PR

定制化解析的标记

JavaScript 工具现在可以模拟“混合”解析规则,就像我们上面描述的 bundler 模式一样。 由于工具的支持可能有所不同,因此 TypeScript 5.0 提供了启用或禁用一些功能的方法,这些功能可能无法与您的配置一起使用。

allowImportingTsExtensions

--allowImportingTsExtensions 允许 TypeScript 文件导入使用了 TypeScript 特定扩展名的文件,例如 .ts, .mts, .tsx

此标记仅在启用了 --noEmit--emitDeclarationOnly 时允许使用, 因为这些导入路径无法在运行时的 JavaScript 输出文件中被解析。 这里的期望是,您的解析器(例如打包工具、运行时或其他工具)将保证这些在 .ts 文件之间的导入可以工作。

resolvePackageJsonExports

--resolvePackageJsonExports 强制 TypeScript 使用 package.json 里的 exports 字段,如果它尝试读取 node_modules 里的某个包。

--moduleResolutionnode16, nodenextbundler 时,该选项的默认值为 true

resolvePackageJsonImports

--resolvePackageJsonImports 强制 TypeScript 使用 package.json 里的 imports 字段,当它查找以 # 开头的文件时,且该文件的父目录中包含 package.json 文件。

--moduleResolutionnode16, nodenextbundler 时,该选项的默认值为 true

allowArbitraryExtensions

在 TypeScript 5.0 中,当导入路径不是以已知的 JavaScript 或 TypeScript 文件扩展名结尾时,编译器将查找该路径的声明文件,形式为 {文件基础名称}.d.{扩展名}.ts。 例如,如果您在打包项目中使用 CSS 加载器,您可能需要编写(或生成)如下的声明文件:

/* app.css */
.cookie-banner {
  display: none;
}
// app.d.css.ts
declare const css: {
  cookieBanner: string;
};
export default css;
// App.tsx
import styles from './app.css';

styles.cookieBanner; // string

默认情况下,该导入将引发错误,告诉您 TypeScript 不支持此文件类型,您的运行时可能不支持导入它。 但是,如果您已经配置了运行时或打包工具来处理它,您可以使用新的 --allowArbitraryExtensions 编译器选项来抑制错误。

需要注意的是,历史上通常可以通过添加名为 app.css.d.ts 而不是 app.d.css.ts 的声明文件来实现类似的效果 - 但是,这只在 Node.js 中 CommonJS 的 require 解析规则下可以工作。 严格来说,前者被解析为名为 app.css.js 的 JavaScript 文件的声明文件。 由于 Node 中的 ESM 需要使用包含扩展名的相对文件导入,因此在 --moduleResolutionnode16nodenext 时,TypeScript 会在示例的 ESM 文件中报错。

更多详情请参考 PR PR

customConditions

--customConditions 接受额外的条件列表,当 TypeScript 从 package.json 的exportsimports 字段解析时,这些条件应该成功。 这些条件会被添加到解析器默认使用的任何现有条件中。

例如,有如下的配置:

{
  "compilerOptions": {
    "target": "es2022",
    "moduleResolution": "bundler",
    "customConditions": ["my-condition"]
  }
}

每当 package.json 里引用了 exportsimports 字段时,TypeScript 都会考虑名为 my-condition 的条件。

所以当从具有如下 package.json 的包中导入时:

{
  // ...
  "exports": {
    ".": {
      "my-condition": "./foo.mjs",
      "node": "./bar.mjs",
      "import": "./baz.mjs",
      "require": "./biz.mjs"
    }
  }
}

TypeScript 会尝试查找 foo.mjs 文件。

该字段仅在 --moduleResolutionnode16, nodenextbundler 时有效。

--verbatimModuleSyntax

在默认情况下,TypeScript 会执行导入省略。 大体上来讲,如果有如下代码:

import { Car } from './car';

export function drive(car: Car) {
  // ...
}

TypeScript 能够检测到导入语句仅用于导入类型,因此会删除导入语句。 最终生成的 JavaScript 代码如下:

export function drive(car) {
  // ...
}

大多数情况下这是没问题的,因为如果 Car 不是从 ./car 导出的值,我们将会得到一个运行时错误。

但在一些特殊情况下,它增加了一层复杂性。 例如,不存在像 import "./car"; 这样的语句 - 这个导入语句会被完全删除。 这对于有副作用的模块来讲是有区别的。

TypeScript 的 JavaScript 代码生成策略还有其它一些复杂性 - 导入省略不仅只是由导入语句的使用方式决定 - 它还取决于值的声明方式。 因此,如下的代码的处理方式不总是那么明显:

export { Car } from './car';

这段代码是应该保留还是删除? 如果 Car 是使用 class 声明的,那么在生成的 JavaScript 代码中会被保留。 但是如果 Car 是使用类型别名或 interface 声明的,那么在生成的 JavaScript 代码中会被省略。

尽管 TypeScript 可以根据多个文件来综合判断如何生成代码,但不是所有的编译器都能够做到。

导入和导出语句中的 type 修饰符能够起到一点作用。 我们可以使用 type 修饰符明确声明导入和导出是否仅用于类型分析,并且可以在生成的 JavaScript 文件中完全删除。

// This statement can be dropped entirely in JS output
import type * as car from './car';

// The named import/export 'Car' can be dropped in JS output
import { type Car } from './car';
export { type Car } from './car';

type 修饰符本身并不是特别管用 - 默认情况下,导入省略仍会删除导入语句, 并且不强制要求您区分类型导入和普通导入以及导出。 因此,TypeScript 提供了 --importsNotUsedAsValues 来确保您使用类型修饰符, --preserveValueImports 来防止某些模块消除行为, 以及 --isolatedModules 来确保您的 TypeScript 代码在不同编译器中都能正常运行。 不幸的是,理解这三个标志的细节很困难,并且仍然存在一些意外行为的边缘情况。

TypeScript 5.0 提供了一个新的 --verbatimModuleSyntax 来简化这个情况。 规则很简单 - 所有不带 type 修饰符的导入导出语句会被保留。 任何带有 type 修饰符的导入导出语句会被删除。

// Erased away entirely.
import type { A } from 'a';

// Rewritten to 'import { b } from "bcd";'
import { b, type c, type d } from 'bcd';

// Rewritten to 'import {} from "xyz";'
import { type xyz } from 'xyz';

使用这个新的选项,实现了所见即所得。

但是,这在涉及模块互操作性时会有一些影响。 在这个标志下,当您的设置或文件扩展名暗示了不同的模块系统时,ECMAScript 的导入和导出不会被重写为 require 调用。 相反,您会收到一个错误。 如果您需要生成使用 requiremodule.exports 的代码,您需要使用早于 ES2015 的 TypeScript 的模块语法:

import foo = require('foo');

// ==>

const foo = require('foo');
function foo() {}
function bar() {}
function baz() {}

export = {
  foo,
  bar,
  baz,
};

// ==>

function foo() {}
function bar() {}
function baz() {}

module.exports = {
  foo,
  bar,
  baz,
};

虽然这是一种限制,但它确实有助于使一些问题更加明显。 例如,在 --module node16 下很容易忘记在 package.json 中设置 type 字段。 结果是开发人员会开始编写 CommonJS 模块而不是 ES 模块,但却没有意识到这一点,从而导致查找规则和 JavaScript 输出出现意外的结果。 这个新的标志确保您有意识地使用文件类型,因为语法是刻意不同的。

因为 --verbatimModuleSyntax 相比于 --importsNotUsedAsValues--preserveValueImports 提供了更加一致的行为,推荐使用前者,后两个标记将被弃用。

更多详情请参考 PRissue.

支持 export type *

在 TypeScript 3.8 引入类型导入时,该语法不支持在 export * from "module"export * as ns from "module" 重新导出上使用。 TypeScript 5.0 添加了对两者的支持:

// models/vehicles.ts
export class Spaceship {
  // ...
}

// models/index.ts
export type * as vehicles from './vehicles';

// main.ts
import { vehicles } from './models';

function takeASpaceship(s: vehicles.Spaceship) {
  //  ok - `vehicles` only used in a type position
}

function makeASpaceship() {
  return new vehicles.Spaceship();
  //         ^^^^^^^^
  // 'vehicles' cannot be used as a value because it was exported using 'export type'.
}

更多详情请参考 PR

支持 JSDoc 中的 @satisfies

TypeScript 4.9 支持 satisfies 运算符。 它确保了表达式的类型是兼容的,且不影响类型自身。 例如,有如下代码:

interface CompilerOptions {
  strict?: boolean;
  outDir?: string;
  // ...
}

interface ConfigSettings {
  compilerOptions?: CompilerOptions;
  extends?: string | string[];
  // ...
}

let myConfigSettings = {
  compilerOptions: {
    strict: true,
    outDir: '../lib',
    // ...
  },

  extends: ['@tsconfig/strictest/tsconfig.json', '../../../tsconfig.base.json'],
} satisfies ConfigSettings;

这里,TypeScript 知道 myConfigSettings.extends 声明为数组 - 因为 satisfies 会验证对象的类型。 因此,如果我们想在 extends 上进行映射操作,那是可以的。

declare function resolveConfig(configPath: string): CompilerOptions;

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

这对 TypeScript 用户来讲是有用处的,但是许多人使用 TypeScript 来对带有 JSDoc 的 JavaScript 代码进行类型检查。 因此,TypeScript 5.0 支持了新的 JSDoc 标签 @satisfies 来做相同的事。

/** @satisfies */ 能够检查出类型不匹配:

// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 */

/**
 * @satisfies {CompilerOptions}
 */
let myCompilerOptions = {
  outdir: '../lib',
  //  ~~~~~~ oops! we meant outDir
};

但它会保留表达式的原始类型,允许我们稍后使用值的更详细的类型。

// @ts-check

/**
 * @typedef CompilerOptions
 * @prop {boolean} [strict]
 * @prop {string} [outDir]
 */

/**
 * @typedef ConfigSettings
 * @prop {CompilerOptions} [compilerOptions]
 * @prop {string | string[]} [extends]
 */

/**
 * @satisfies {ConfigSettings}
 */
let myConfigSettings = {
  compilerOptions: {
    strict: true,
    outDir: '../lib',
  },
  extends: ['@tsconfig/strictest/tsconfig.json', '../../../tsconfig.base.json'],
};

let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

/** @satisfies */ 也可以在行内的括号表达式上使用。 可以像下面这样定义 myConfigSettings

let myConfigSettings = /** @satisfies {ConfigSettings} */ {
  compilerOptions: {
    strict: true,
    outDir: '../lib',
  },
  extends: ['@tsconfig/strictest/tsconfig.json', '../../../tsconfig.base.json'],
};

为什么?当你更深入地研究其他代码时,比如函数调用,它通常更有意义。

compileCode(
  /** @satisfies {ConfigSettings} */ {
    // ...
  }
);

更多详情请参考 PR。 感谢作者 Oleksandr Tarasiuk

支持 JSDoc 中的 @overload

在 TypeScript 中,你可以为一个函数指定多个重载。 使用重载能够描述一个函数可以使用不同的参数进行调用,也可能会返回不同的结果。 它们可以限制调用方如何调用函数,并细化他们将得到的结果。

// Our overloads:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;

// Our implementation:
function printValue(value: string | number, maximumFractionDigits?: number) {
  if (typeof value === 'number') {
    const formatter = Intl.NumberFormat('en-US', {
      maximumFractionDigits,
    });
    value = formatter.format(value);
  }

  console.log(value);
}

这里表示 printValue 的第一个参数可以为 stringnumber 类型。 如果接收的是 number 类型,那么它还接收第二个参数决定打印的小数位数。

TypeScript 5.0 支持在 JSDoc 里使用 @overload 来声明重载。 每一个 JSDoc @overload 标记都表示一个不同的函数重载。

// @ts-check

/**
 * @overload
 * @param {string} value
 * @return {void}
 */

/**
 * @overload
 * @param {number} value
 * @param {number} [maximumFractionDigits]
 * @return {void}
 */

/**
 * @param {string | number} value
 * @param {number} [maximumFractionDigits]
 */
function printValue(value, maximumFractionDigits) {
  if (typeof value === 'number') {
    const formatter = Intl.NumberFormat('en-US', {
      maximumFractionDigits,
    });
    value = formatter.format(value);
  }

  console.log(value);
}

现在不论是编写 TypeScript 文件还是 JavaScript 文件,TypeScript 都能够提示函数调用是否正确。

// all allowed
printValue('hello!');
printValue(123.45);
printValue(123.45, 2);

printValue('hello!', 123); // error!

更多详情请参考 PR,感谢 Tomasz Lenarcik

--build 模式下使用有关文件生成的选项

TypeScript 现在允许在 --build 模式下使用如下选项:

  • --declaration
  • --emitDeclarationOnly
  • --declarationMap
  • --sourceMap
  • --inlineSourceMap

这使得在构建过程中定制某些部分变得更加容易,特别是在你可能会有不同的开发和生产构建时。

例如,一个库的开发构建可能不需要生成声明文件,但是生产构建则需要。 一个项目可以将生成声明文件配置为默认关闭,并使用如下方式构建:

tsc --build -p ./my-project-dir

开发完毕后,在“生产环境”构建时使用 --declaration 选项:

tsc --build -p ./my-project-dir --declaration

更多详情请参考 PR

编辑器导入语句排序时不区分大小写

在 Visual Studio 和 VS Code 等编辑器中,TypeScript 可以帮助组织和排序导入和导出语句。 不过,通常情况下,对于何时将列表“排序”,可能会有不同的解释。

例如,下面的导入列表是否已排序?

import { Toggle, freeze, toBoolean } from './utils';

令人惊讶的是,答案可能是“这要看情况”。 如果我们不考虑大小写敏感性,那么这个列表显然是没有排序的。 字母f排在tT之前。

但在大多数编程语言中,排序默认是比较字符串的字节值。 JavaScript 比较字符串的方式意味着 “Toggle” 总是排在 “freeze” 之前,因为根据 ASCII 字符编码,大写字母排在小写字母之前。 所以从这个角度来看,导入列表是已排序的。

以前,TypeScript 认为导入列表已排序,因为它进行了基本的大小写敏感排序。 这可能让开发人员感到沮丧,因为他们更喜欢不区分大小写的排序方式,或者使用像 ESLint 这样的工具默认需要不区分大小写的排序方式。

现在,TypeScript 默认会检测大小写敏感性。 这意味着 TypeScript 和类似 ESLint 的工具通常不会因为如何最好地排序导入而“互相冲突”。

我们的团队还在尝试更多的排序策略,你可以在这里了解更多。 这些选项可能最终可以由编辑器进行配置。 目前,它们仍然不稳定和实验性的,你可以通过在 JSON 选项中使用 typescript.unstable 条目来选择它们。 下面是你可以尝试的所有选项(设置为它们的默认值):

{
  "typescript.unstable": {
    // Should sorting be case-sensitive? Can be:
    // - true
    // - false
    // - "auto" (auto-detect)
    "organizeImportsIgnoreCase": "auto",

    // Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
    // - "ordinal"
    // - "unicode"
    "organizeImportsCollation": "ordinal",

    // Under `"organizeImportsCollation": "unicode"`,
    // what is the current locale? Can be:
    // - [any other locale code]
    // - "auto" (use the editor's locale)
    "organizeImportsLocale": "en",

    // Under `"organizeImportsCollation": "unicode"`,
    // should upper-case letters or lower-case letters come first? Can be:
    // - false (locale-specific)
    // - "upper"
    // - "lower"
    "organizeImportsCaseFirst": false,

    // Under `"organizeImportsCollation": "unicode"`,
    // do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
    // - true
    // - false
    "organizeImportsNumericCollation": true,

    // Under `"organizeImportsCollation": "unicode"`,
    // do letters with accent marks/diacritics get sorted distinctly
    // from their "base" letter (i.e. is é different from e)? Can be
    // - true
    // - false
    "organizeImportsAccentCollation": true
  },
  "javascript.unstable": {
    // same options valid here...
  }
}

更多详情请参考 PRPR

穷举式 switch/case 自动补全

在编写 switch 语句时,TypeScript 现在会检测被检查的值是否具有字面量类型。 如果是,它将提供一个补全选项,可以为每个未覆盖的情况构建骨架代码。

更多详情请参考 PR

速度,内存以及代码包尺寸优化

TypeScript 5.0 在我们的代码结构、数据结构和算法实现方面进行了许多强大的变化。 这些变化的意义在于,整个体验都应该更快 —— 不仅仅是运行 TypeScript,甚至包括安装 TypeScript。

以下是我们相对于 TypeScript 4.9 能够获得的一些有趣的速度和大小优势。

ScenarioTime or Size Relative to TS 4.9
material-ui build time90%
TypeScript Compiler startup time89%
Playwright build time88%
TypeScript Compiler self-build time87%
Outlook Web build time82%
VS Code build time80%
typescript npm Package Size59%

img

img

怎么做到的呢?我们将在未来的博客文章中详细介绍一些值得注意的改进。 但我们不会让你等到那篇博客文章。

首先,我们最近将 TypeScript 从命名空间迁移到了模块,这使我们能够利用现代构建工具来执行像作用域提升这样的优化。 使用这些工具,重新审视我们的打包策略,并删除一些已过时的代码,使 TypeScript 4.9 的 63.8 MB 包大小减少了约 26.4 MB。 这也通过直接函数调用为我们带来了显著的加速。 我们在这里撰写了关于我们迁移到模块的详细介绍

TypeScript 还在编译器内部对象类型上增加了更多的一致性,并且也减少了一些这些对象类型上存储的数据。 这减少了多态操作,同时平衡了由于使我们的对象结构更加一致而带来的内存使用增加。

我们还在将信息序列化为字符串时执行了一些缓存。 类型显示,它可能在错误报告、声明生成、代码补全等情况下使用,是非常昂贵的操作。 TypeScript 现在对一些常用的机制进行缓存,以便在这些操作之间重复使用。

我们进行了一个值得注意的改变,改善了我们的解析器,即在某些情况下,利用 var 来避免在闭包中使用 let 和 const 的成本。 这提高了一些解析性能。

总的来说,我们预计大多数代码库应该会从 TypeScript 5.0 中看到速度的提升,并且一直能够保持 10% 到 20% 之间的优势。 当然,这将取决于硬件和代码库的特性,但我们鼓励你今天就在你的代码库上尝试它!

更多详情:

TypeScript 4.9

satisfies 运算符

TypeScript 开发者有时会感到进退两难:既想要确保表达式能够匹配某种类型,也想要表达式获得最确切的类型用作类型推断。

例如:

// 每个属性可能是 string 或 RGB 元组。
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ^^^^ 拼写错误
};

// 我们想要在 'red' 上调用数组的方法
const redComponent = palette.red.at(0);

// 或者在 'green' 上调用字符串的方法
const greenNormalized = palette.green.toUpperCase();

注意,这里写成了 bleu,但我们想写的是 blue。 通过给 palette 添加类型注释就能够捕获 bleu 拼写错误, 但同时我们也失去了属性各自的信息。

type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette: Record<Colors, string | RGB> = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ 能够检测到拼写错误
};

// 意想不到的错误 - 'palette.red' 可能为 string
const redComponent = palette.red.at(0);

新的 satisfies 运算符让我们可以验证表达式是否匹配某种类型,同时不改变表达式自身的类型。 例如,可以使用 satisfies 来检验 palette 的所有属性与 string | number[] 是否兼容:

type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ 捕获拼写错误
} satisfies Record<Colors, string | RGB>;

// 依然可以访问这些方法
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

satisfies 可以用来捕获许多错误。 例如,检查一个对象是否包含了某个类型要求的所有的键,并且没有多余的:

type Colors = "red" | "green" | "blue";

// 确保仅包含 'Colors' 中定义的键
const favoriteColors = {
    "red": "yes",
    "green": false,
    "blue": "kinda",
    "platypus": false
//  ~~~~~~~~~~ 错误 - "platypus" 不在 'Colors' 中
} satisfies Record<Colors, unknown>;

// 'red', 'green', and 'blue' 的类型信息保留下来
const g: boolean = favoriteColors.green;

有可能我们不太在乎属性名,在乎的是属性值的类型。 在这种情况下,我们也能够确保对象属性值的类型是匹配的。

type RGB = [red: number, green: number, blue: number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0]
    //    ~~~~~~ 错误!
} satisfies Record<string, string | RGB>;

// 类型信息保留下来
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

更多示例请查看这里这里。 感谢Oleksandr Tarasiuk对该属性的贡献。

使用 in 运算符来细化并未列出其属性的对象类型

开发者经常需要处理在运行时不完全已知的值。 事实上,我们常常不能确定对象的某个属性是否存在,是否从服务端得到了响应或者读取到了某个配置文件。 JavaScript 的 in 运算符能够检查对象上是否存在某个属性。

从前,TypeScript 能够根据没有明确列出的属性来细化类型。

interface RGB {
    red: number;
    green: number;
    blue: number;
}

interface HSV {
    hue: number;
    saturation: number;
    value: number;
}

function setColor(color: RGB | HSV) {
    if ("hue" in color) {
        // 'color' 类型为 HSV
    }
    // ...
}

此处,RGB 类型上没有列出 hue 属性,因此被细化掉了,剩下了 HSV 类型。

那如果每个类型上都没有列出这个属性呢? 在这种情况下,语言无法提供太多的帮助。 看下面的 JavaScript 示例:

function tryGetPackageName(context) {
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            return packageJSON.name;
        }
    }

    return undefined;
}

将上面的代码改写为合适的 TypeScript,我们会给 context 定义一个类型; 然而,在旧版本的 TypeScript 中如果声明 packageJSON 属性的类型为安全的 unknown 类型会有问题。

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context) {
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
        //                                              ~~~~
        // error! Property 'name' does not exist on type 'object.
            return packageJSON.name;
        //                     ~~~~
        // error! Property 'name' does not exist on type 'object.
        }
    }

    return undefined;
}

这是因为当 packageJSON 的类型从 unknown 细化为 object 类型后, in 运算符会严格地将类型细化为包含了所检查属性的某个类型。 因此,packageJSON 的类型仍为 object

TypeScript 4.9 增强了 in 运算符的类型细化功能,它能够更好地处理没有列出属性的类型。 现在 TypeScript 不是什么也不做,而是将其类型与 Record<"property-key-being-checked", unknown> 进行类型交叉运算。

因此在上例中,packageJSON 的类型将从 unknown 细化为 object 再细化为 object & Record<"name", unknown>。 这样就允许我们访问并细化类型 packageJSON.name

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context): string | undefined {
    const packageJSON = context.packageJSON;
    // Check to see if we have an object.
    if (packageJSON && typeof packageJSON === "object") {
        // Check to see if it has a string name property.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            // Just works!
            return packageJSON.name;
        }
    }

    return undefined;
}

TypeScript 4.9 还会严格限制 in 运算符的使用,以确保左侧的操作数能够赋值给 string | number | symbol,右侧的操作数能够赋值给 object。 它有助于检查是否使用了合法的属性名,以及避免在原始类型上进行检查。

更多详情请查看 PR.

类中的自动存取器

TypeScript 4.9 支持了 ECMAScript 即将引入的“自动存取器”功能。 自动存取器的声明如同定义一个类的属性,只不过是需要使用 accessor 关键字。

class Person {
    accessor name: string;

    constructor(name: string) {
        this.name = name;
    }
}

在底层实现中,自动存取器会被展开为 getset 存取器,以及一个无法访问的私有成员。

class Person {
    #__name: string;

    get name() {
        return this.#__name;
    }
    set name(value: string) {
        this.#__name = name;
    }

    constructor(name: string) {
        this.name = name;
    }
}

更多详情请参考 PR

NaN 上的相等性检查

在 JavaScript 中,你无法使用内置的相等运算符去检查某个值是否等于 NaN

由于一些原因,NaN 是个特殊的数值,它代表 不是一个数字。 没有值等于 NaN,包括 NaN 自己!

console.log(NaN == 0)  // false
console.log(NaN === 0) // false

console.log(NaN == NaN)  // false
console.log(NaN === NaN) // false

换句话说,任何值都不等于 NaN

console.log(NaN != 0)  // true
console.log(NaN !== 0) // true

console.log(NaN != NaN)  // true
console.log(NaN !== NaN) // true

从技术上讲,这不是 JavaScript 独有的问题,任何使用 IEEE-754 浮点数的语言都有一样的问题; 但是 JavaScript 中主要的数值类型为浮点数,并且解析数值时经常会得到 NaN。 因此,检查 NaN 是很常见的操作,正确的方法是使用 Number.isNaN 函数 - 但像上文提到的,很多人可能不小心地使用了 someValue === NaN 来进行检查。

现在,如果 TypeScript 发现直接比较 NaN 会报错,并提示使用 Number.isNaN

function validate(someValue: number) {
    return someValue !== NaN;
    //     ~~~~~~~~~~~~~~~~~
    // error: This condition will always return 'true'.
    //        Did you mean '!Number.isNaN(someValue)'?
}

我们确信这个改动会帮助捕获初级的错误,就如同 TypeScript 也会检查比较对象字面量和数组字面量一样。

感谢 Oleksandr Tarasiuk 提交的 PR

监视文件功能使用文件系统事件

在先前的版本中,TypeScript 主要依靠轮询来监视每个文件。 使用轮询的策略意味着定期检查文件是否有更新。 在 Node.js 中,fs.watchFile 是内置的使用轮询来检查文件变动的方法。 虽说轮询在跨操作系统和文件系统的情况下更稳妥,但是它也意味着 CPU 会定期地被中断,转而去检查是否有文件更新即便在没有任何改动的情况下。 这在只有少数文件的时候问题不大,但如果工程包含了大量文件 - 或 node_modules 里有大量的文件 - 就会变得非常吃资源。

通常来讲,更好的做法是使用文件系统事件。 做为轮询的替换,我们声明对某些文件的变动感兴趣并提供回调函数用于处理有改动的文件。 大多数现代的平台提供了如 CreateIoCompletionPortkqueueepollinotify API。 Node.js 对这些 API 进行了抽象,提供了 fs.watch API。 文件系统事件通常可以很好地工作,但是也存在一些注意事项。 一个 watcher 需要考虑 inode watching的问题、 在一些文件系统上不可用的问题(比如:网络文件系统)、 嵌套的文件监控是否可用、重命名目录是否触发事件以及可用 file watcher 耗尽的问题! 换句话说,这件事不是那么容易做的,特别是我们还需要跨平台。

因此,过去我们的默认选择是普遍好用的方式:轮询。 虽不总是,但大部分时候是这样的。

后来,我们提供了选择文件监视策略的方法。 这让我们收到了很多使用反馈并改善跨平台的问题。 由于 TypeScript 必须要能够处理大规模的代码并且也已经有了改进,因此我们觉得切换到使用文件系统事件是件值得做的事情。

在 TypeScript 4.9 中,文件监视已经默认使用文件系统事件的方式,仅当无法初始化事件监视时才回退到轮询。 对大部分开发者来讲,在使用 --watch 模式或在 Visual Studio、VS Code 里使用 TypeScript 时会极大降低资源的占用。

文件监视方式仍然是可以配置的,可以使用环境变量和 watchOptions - 像 VS Code 这样的编辑器还支持单独配置。 如果你的代码使用的是网络文件系统(如 NFS 和 SMB)就需要回退到旧的行为; 但如果服务器有强大的处理能力,最好是启用 SSH 并且通过远程运行 TypeScript,这样就可以使用本地文件访问。 VS Code 支持了很多远程开发的工具。

编辑器中的“删除未使用导入”和“排序导入”命令

以前,TypeScript 仅支持两个管理导入语句的编辑器命令。 拿下面的代码举例:

import { Zebra, Moose, HoneyBadger } from "./zoo";
import { foo, bar } from "./helper";

let x: Moose | HoneyBadger = foo();

第一个命令是 “组织导入语句”,它会删除未使用的导入并对剩下的条目排序。 因此会将上面的代码重写为:

import { foo } from "./helper";
import { HoneyBadger, Moose } from "./zoo";

let x: Moose | HoneyBadger = foo();

在 TypeScript 4.3 中,引入了“排序导入语句”命令,它仅排序导入语句但不进行删除,因此会将上例代码重写为:

import { bar, foo } from "./helper";
import { HoneyBadger, Moose, Zebra } from "./zoo";

let x: Moose | HoneyBadger = foo();

使用“排序导入语句”的注意事项是,在 VS Code 中该命令只能在保存文件时触发,而非能够手动执行的命令。

TypeScript 4.9 添加了另一半功能,提供了“移除未使用的导入”功能。 TypeScript 会移除未使用的导入命名和语句,但是不能改变当前的排序。

import { Moose, HoneyBadger } from "./zoo";
import { foo } from "./helper";

let x: Moose | HoneyBadger = foo();

该功能对任何编译器都是可用的; 但要注意的是,VS Code (1.73+) 会内置这个功能并且可以使用 Command Pallette 来执行。 如果用户想要使用更细的“移除未使用的导入”或“排序导入”命令,那么可以将“组织导入”的快捷键绑定到这些命令上。

更多详情请参考这里

return 关键字上使用跳转到定义

在编辑器中,当在 return 关键字上使用跳转到定义功能时,TypeScript 会跳转到函数的顶端。 这会帮助理解 return 语句是属于哪个函数的。

我们期待这个功能扩展到更多的关键字上,例如 awaityield 或者 switchcasedefault。 感谢Oleksandr Tarasiuk实现

性能优化

TypeScript 进行了一些较小的但是能觉察到的性能优化。

首先,重写了 TypeScript 的 forEachChild 函数使用函数查找表代替 switch 语句。 forEachChild 是编译器在遍历语法节点时会反复调用的函数,和部分语言服务一起大量地被使用在编译绑定阶段。 对 forEachChild 函数的重构减少了绑定阶段和语言服务操作的 20% 时间消耗。

当我们看到了 forEachChild 的效果后也在 visitEachChild(在编译器和语言服务中用来变换节点的函数)上进行了类似的优化。 同样的重构减少了 3% 生成工程输出的时间消耗。

对于 forEachChild 的优化最初是受到了 Artemis Everfree 文章的启发。 虽说我们认为速度提升的根本原因是由于函数体积和复杂度的降低而非这篇文章里提到的问题,但我们非常感谢能够从中获得经验并快速地进行重构让 TypeScript 运行得更快。

最后,TypeScript 还优化了在条件类型的 true 分支中保留类型信息。 例如:

interface Zoo<T extends Animal> {
    // ...
}

type MakeZoo<A> = A extends Animal ? Zoo<A> : never;

TypeScript 在检查 Zoo<A>时需要记住 AAnimal。 TypeScript 通过新建 AAnimal 的交叉类型来保留该信息; 然而,TypeScript 之前采用的是即时求值的方式,即便有时是不需要的。 而且类型检查器中的一些问题代码使得这些类型无法被简化。 TypeScript 现在会推迟类型交叉操作直到真的有需要的时候。 对于大量地使用了有条件类型的代码来说,你会觉察到大幅的提速,但从我们的性能测试结果来看却只看到了 3% 的类型检查性能提升。

TypeScript 4.8

改进的交叉类型化简、联合类型兼容性以及类型细化

TypeScript 4.8 为 --strictNullChecks 带来了一系列修正和改进。 这些变化会影响交叉类型和联合类型的工作方式,也作用于 TypeScript 的类型细化。

例如,unknown{} | null | undefined 类型神似, 因为它接受 nullundefined 以及任何其它类型。 TypeScript 现在能够识别出这种情况,允许将 unknown 赋值给 {} | null | undefined

译者注:除 nullundefined 类型外,其它任何类型都可以赋值给 {} 类型。

function f(x: unknown, y: {} | null | undefined) {
    x = y; // 可以工作
    y = x; // 以前会报错,现在可以工作
}

另一个变化是 {} 与任何其它对象类型交叉会得到那个对象类型。 因此,我们可以重写 NonNullable 类型为与 {} 的交叉类型, 因为 {} & null{} & undefined 会被消掉。

- type NonNullable<T> = T extends null | undefined ? never : T;
+ type NonNullable<T> = T & {};

之所以称其为一项改进,是因为交叉类型可以被化简和赋值了, 而有条件类型目前是不支持的。 因此,NonNullable<NonNullable<T>> 至少可以简化为 NonNullable<T>,在以前这是不行的。

function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
    x = y; // 一直没问题
    y = x; // 以前会报错,现在没问题
}

这些变化还为我们带来了更合理的控制流分析和类型细化。 比如,unknown 在条件为“真”的分支中被细化为 {} | null | undefined

function narrowUnknownishUnion(x: {} | null | undefined) {
    if (x) {
        x;  // {}
    }
    else {
        x;  // {} | null | undefined
    }
}

function narrowUnknown(x: unknown) {
    if (x) {
        x;  // 以前是 'unknown',现在是 '{}'
    }
    else {
        x;  // unknown
    }
}

泛型也会进行类似的细化。 当检查一个值不为 nullundefined 时, TypeScript 会将其与 {} 进行交叉 - 等同于使用 NonNullable。 把所有变化放在一起,我们就可以在不使用类型断言的情况下定义下列函数。

function throwIfNullable<T>(value: T): NonNullable<T> {
    if (value === undefined || value === null) {
        throw Error("Nullable value!");
    }

    // 以前会报错,因为 'T' 不能赋值给 'NonNullable<T>'。
    // 现在会细化为 'T & {}' 并且不报错,因为它等同于 'NonNullable<T>'。
    return value;
}

value 细化为了 T & {},此时它与 NonNullable<T> 等同 - 因此在函数体中不再需要使用 TypeScript 的特定语法。

就该改进本身而言可能是一个很小的变化 - 但它却实实在在地修复了在过去几年中报告的大量问题。

更多详情,请参考这里

改进模版字符串类型中 infer 类型的类型推断

近期,TypeScript 支持了在有条件类型中的 infer 类型变量上添加 extends 约束。

// 提取元组类型中的第一个元素,若其能够赋值给 'number',
// 返回 'never' 若无这样的元素。
type TryGetNumberIfFirst<T> =
    T extends [infer U extends number, ...unknown[]] ? U : never;

infer 类型出现在模版字符串类型中且被原始类型所约束,则 TypeScript 会尝试将其解析为字面量类型。

// SomeNum 以前是 'number';现在是 '100'。
type SomeNum = "100" extends `${infer U extends number}` ? U : never;

// SomeBigInt 以前是 'bigint';现在是 '100n'。
type SomeBigInt = "100" extends `${infer U extends bigint}` ? U : never;

// SomeBool 以前是 'boolean';现在是 'true'。
type SomeBool = "true" extends `${infer U extends boolean}` ? U : never;

现在它能更好地表达代码库在运行时的行为,提供更准确的类型。

要注意的一点是当 TypeScript 解析这些字面量类型时会使用贪心策略,尽可能多地提取原始类型; 然后再回头检查解析出的原始类型是否匹配字符串的内容。 也就是说,TypeScript 检查从字符串到原始类型再到字符串是否匹配。 如果发现字符串前后对不上了,那么回退到基本的原始类型。

// JustNumber 为 `number` 因为 TypeScript 解析 出 `"1.0"`,但 `String(Number("1.0"))` 为 `"1"` 不匹配。
type JustNumber = "1.0" extends `${infer T extends number}` ? T : never; 

更多详情请参考这里

--build, --watch, 和 --incremental 的性能优化

TypeScript 4.8 优化了使用 --watch--incremental 时的速度,以及使用 --build 构建工程引用时的速度。 例如,现在在 --watch 模式下 TypeScript 不会去更新未改动文件的时间戳, 这使得重新构建更快,避免与其它监视 TypeScript 输出文件的构建工具之间产生干扰。 此外,TypeScript 也能够重用 --build, --watch--incremental 之间的信息。

这些优化有多大效果?在一个相当大的代码库上,对于简单常用的操作有 10%-25% 的改进,对于无改动操作的场景节省了 40% 的时间。 在 TypeScript 代码库中我们也看到了相似的结果。

更多详情请参考这里

比较对象和数组字面量时报错

在许多语言中,== 操作符在对象上比较的是“值”。 例如,在 Python 语言中想检查列表是否为空时可以使用 == 检查该值是否与空列表相等。

if people_at_home == []:
    print("that's where she lies, broken inside. </3")

在 JavaScript 里却不是这样,使用 ===== 比较对象和数组时比较的是引用。 我们确信这会让 JavaScript 程序员搬起石头砸自己脚,且最坏的情况是在生产环境中存在 bug。 因此,TypeScript 现在不允许如下的代码:

let peopleAtHome = [];

if (peopleAtHome === []) {
//  ~~~~~~~~~~~~~~~~~~~
// This condition will always return 'false' since JavaScript compares objects by reference, not value.
    console.log("that's where she lies, broken inside. </3")
}

非常感谢Jack Works的贡献。 更多详情请参考这里

改进从绑定模式中进行类型推断

在某些情况下,TypeScript 会从绑定模式中获取类型来帮助类型推断。

declare function chooseRandomly<T>(x: T, y: T): T;

let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]);
//   ^  ^  ^
//   |  |  |
//   |  |  string
//   |  |
//   |  boolean
//   |
//   number

chooseRandomly 需要确定 T 的类型时,它主要检查 [42, true, "hi!"][0, false, "bye!"]; 但 TypeScript 还需要确定这两个类型是 Array<number | boolean | string> 还是 [number, boolean, string]。 为此,它会检查当前类型推断候选列表中是否存在元组类型。 当 TypeScript 看到了绑定模式 [a, b, c],它创建了类型 [any, any, any], 该类型会被加入到 T 的候选列表(作为推断 [42, true, "hi!"][0, false, "bye!"] 的参考)但优先级较低。

这对 chooseRandomly 函数来讲不错,但在有些情况下不合适。例如:

declare function f<T>(x?: T): T;

let [x, y, z] = f();

绑定模式 [x, y, z] 提示 f 应该输出 [any, any, any] 元组; 但是 f 不应该根据绑定模式来改变类型参数的类型。 它不应该像变戏法一样根据被赋的值突然变成一个类数组的值, 因此绑定模式过多地影响到了生成的类型。 由于绑定模式中均为 any 类型,因此我们也就让 xyzany 类型。

在 TypeScript 4.8 里,绑定模式不会成为类型参数的候选类型。 它们仅在参数需要更确切的类型时提供参考,例如 chooseRandomly 的情况。 如果你想回到之前的行为,可以提供明确的类型参数。

更多详情请参考这里

修复文件监视(尤其是在 git checkout 之间)

长久以来 TypeScript 中存在一个 bug,它对在编辑器中使用 --watch 模式监视文件改动处理的不好。 它有时表现为错误提示不准确,需要重启 tsc 或 VS Code 才行。 这在 Unix 系统上常发生,例如用 vim 保存了一个文件或切换了 git 的分支。

这是因为错误地假设了 Node.js 在不同文件系统下处理文件重命名的方式。 Linux 和 macOS 使用 inodesNode.js 监视的是 inodes 的变化而非文件路径。 因此,当 Node.js 返回了 watcher 对象, 根据平台和文件系统的不同,它即可能监视文件路径也可能是 inode。

为了高效,TypeScript 尝试重用 watcher 对象,如果它检测到文件路径仍存在于磁盘上。 这里就产生了问题,因为即使给定路径上的文件仍然存在,但它可能是全新创建的文件,inode 已经发生了变化。 TypeScript 重用了 watcher 对象而非重新创建一个 watcher 对象,因此可能监视了一个完全不相关的文件。 TypeScript 4.8 能够在 inode 系统上处理这些情况,新建 watcher 对象。

非常感谢 Marc Celani 和他的团队的贡献。 更多详情请参考这里

查找所有引用性能优化

在编辑器中执行“查找所有引用”时,TypeScript 现在能够更智能地聚合引用。 在 TypeScript 自己的代码库中去搜索一个广泛使用的标识符时能够减少 20% 时间。

更多详情请参考这里

从自动导入中排除指定文件

TypeScript 4.8 增加了一个编辑器首选项从自动导入中排除指定文件。 在 Visual Studio Code 里,可以将文件名和 globs 添加到 Settings UI 的 “Auto Import File Exclude Patterns” 下,或者 .vscode/settings.json 文件中:

{
    // Note that `javascript.preferences.autoImportFileExcludePatterns` can be specified for JavaScript too.
    "typescript.preferences.autoImportFileExcludePatterns": [
      "**/node_modules/@types/node"
    ]
}

如果你想避免导入某些模块或代码库,它个功能就派上用场了。 有些模块可能有过多的导出以致于影响到了自动导入功能,让我们难以选择一条自动导入。

更多详情请参考这里

TypeScript 4.7

Node.js 对 ECMAScript Module 的支持

在过去的几年中,Node.js 为支持 ECMAScript 模块(ESM)而做了一些工作。 这是一项有难度的工作,因为 Node.js 生态圈是基于 CommonJS(CJS)模块系统构建的,而非 ESM。 支持两者之间的互操作带来了巨大挑战,有大量的特性需要考虑; 然而,在 Node.js 12 及以上版本中,已经提供了对 ESM 的大部分支持。 在 TypeScript 4.5 期间的一个 nightly 版本中支持了在 Node.js 里使用 ESM 以获得用户反馈, 同时让代码库作者们有时间为此提前作准备。

TypeScript 4.7 正式地支持了该功能,它添加了两个新的 module 选项:node16nodenext

{
    "compilerOptions": {
        "module": "node16",
    }
}

这些新模式带来了一些高级特征,下面将一一介绍。

package.json 里的 type 字段和新的文件扩展名

Node.js 在 package.json 中支持了一个新的设置,叫做 type"type" 可以被设置为 "module" 或者 "commonjs"

{
    "name": "my-package",
    "type": "module",

    "//": "...",
    "dependencies": {
    }
}

这些设置会控制 .js 文件是作为 ESM 进行解析还是作为 CommonJS 模块进行解析, 若没有设置,则默认值为 CommonJS。 当一个文件被当做 ESM 模块进行解析时,会使用如下与 CommonJS 模块不同的规则:

  • 允许使用 import / export 语句
  • 允许使用顶层的 await
  • 相对路径导入必须提供完整的扩展名(需要使用 import "./foo.js" 而非 import "./foo"
  • 解析 node_modules 里的依赖可能不同
  • 不允许直接使用像 requiremodule 这样的全局值
  • 需要使用特殊的规则来导入 CommonJS 模块

我们回头会介绍其中一部分。

为了让 TypeScript 融入该系统,.ts.tsx 文件现在也以同样的方式工作。 当 TypeScript 遇到 .ts.tsx.js.jsx 文件时, 它会向上查找 package.json 来确定该文件是否使用了 ESM,然后再以此决定:

  • 如何查找该文件所导入的其它模块
  • 当需要产生输出的时,如何转换该文件

当一个 .ts 文件被编译为 ESM 时,ECMAScript import / export 语句在生成的 .js 文件中原样输出; 当一个 .ts 文件被编译为 CommonJS 模块时,则会产生与使用了 --module commonjs 选项一致的输出结果。

这也意味着 ESM 和 CJS 模块中的 .ts 文件路径解析是不同的。 例如,现在有如下的代码:

// ./foo.ts
export function helper() {
    // ...
}

// ./bar.ts
import { helper } from "./foo"; // only works in CJS

helper();

这段代码在 CommonJS 模块里没问题,但在 ESM 里会出错,因为相对导入需要使用完整的扩展名。 因此,我们不得不重写代码并使用 foo.ts 输出文件的扩展名,bar.ts 必须从 ./foo.js 导入。

// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS

helper();

初看可能感觉很繁琐,但 TypeScript 的自动导入工具以及路径补全工具会有所帮助。

此外还需要注意的是该行为同样适用于 .d.ts 文件。 当 TypeScript 在一个 package 里找到了 .d.ts 文件,它会基于这个 package 来解析 .d.ts 文件。

新的文件扩展名

package.json 文件里的 type 字段让我们可以继续使用 .ts.js 文件扩展名; 但你可能偶尔需要编写与 type 设置不符的文件,或者更喜欢明确地表达意图。

为此,Node.js 支持了两个文件扩展名:.mjs.cjs.mjs 文件总是使用 ESM,而 .cjs 则总是使用 CommonJS 模块, 它们分别会生成 .mjs.cjs 文件。

正因此,TypeScript 也支持了两个新的文件扩展名:.mts.cts。 当 TypeScript 生成 JavaScript 文件时,将生成 .mjs.cjs

TypeScript 还支持了两个新的声明文件扩展名:.d.mts.d.cts。 当 TypeScript 为 .mts.cts 生成声明文件时,对应的扩展名为 .d.mts.d.cts

这些扩展名的使用完全是可选的,但通常是有帮助的,不论它们是不是你工作流中的一部分。

CommonJS 互操作性

Node.js 允许 ESM 导入 CommonJS 模块,就如同它们是带有默认导出的 ESM。

// ./foo.cts
export function helper() {
    console.log("hello world!");
}

// ./bar.mts
import foo from "./foo.cjs";

// prints "hello world!"
foo.helper();

在某些情况下,Node.js 会综合和合成 CommonJS 模块里的命名导出,这提供了便利。 此时,ESM 既可以使用“命名空间风格”的导入(例如,import * as foo from "..."), 也可以使用命名导入(例如,import { helper } from "...")。

// ./foo.cts
export function helper() {
    console.log("hello world!");
}

// ./bar.mts
import { helper } from "./foo.cjs";

// prints "hello world!"
helper();

有时候 TypeScript 不知道命名导入是否会被综合合并,但如果 TypeScript 能够通过确定地 CommonJS 模块导入了解到该信息,那么就会提示错误。

关于互操作性,TypeScript 特有的注意点是如下的语法:

import foo = require("foo");

在 CommonJS 模块中,它可以归结为 require() 调用, 在 ESM 里,它会导入 createRequire 来完成同样的事情。 对于像浏览器这样的平台(不支持 require())这段代码的可移植性较差,但对互操作性是有帮助的。 你可以这样改写:

// ./foo.cts
export function helper() {
    console.log("hello world!");
}

// ./bar.mts
import foo = require("./foo.cjs");

foo.helper()

最后值得注意的是在 CommonJS 模块里导入 ESM 的唯一方法是使用动态 import() 调用。 这也许是一个挑战,但也是目前 Node.js 的行为。

更多详情,请阅读这里

package.json 中的 exports, imports 以及自引用

Node.js 在 package.json 支持了一个新的字段 exports 来定义入口位置。 它比在 package.json 里定义 "main" 更强大,它能控制将包里的哪些部分公开给使用者。

下例的 package.json 支持对 CommonJS 和 ESM 使用不同的入口位置:

// package.json
{
    "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for `import "my-package"` in ESM
            "import": "./esm/index.js",

            // Entry-point for `require("my-package") in CJS
            "require": "./commonjs/index.cjs",
        },
    },

    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs",
}

关于该特性的更多详情请阅读这里。 下面我们主要关注 TypeScript 是如何支持它的。

在以前 TypeScript 会先查找 "main" 字段,然后再查找其对应的声明文件。 例如,如果 "main" 指向了 ./lib/index.js, TypeScript 会查找名为 ./lib/index.d.ts 的文件。 代码包作者可以使用 "types" 字段来控制该行为(例如,"types": "./types/index.d.ts")。

新实现的工作方式与导入条件相似。 默认地,TypeScript 使用与导入条件相同的规则 - 对于 ESM 里的 import 语句,它会查找 import 字段; 对于 CommonJS 模块里的 import 语句,它会查找 require 字段。 如果找到了文件,则去查找相应的声明文件。 如果你想将声明文件指向其它位置,则可以添加一个 "types" 导入条件。

// package.json
{
    "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for `import "my-package"` in ESM
            "import": {
                // Where TypeScript will look.
                "types": "./types/esm/index.d.ts",

                // Where Node.js will look.
                "default": "./esm/index.js"
            },
            // Entry-point for `require("my-package") in CJS
            "require": {
                // Where TypeScript will look.
                "types": "./types/commonjs/index.d.cts",

                // Where Node.js will look.
                "default": "./commonjs/index.cjs"
            },
        }
    },

    // Fall-back for older versions of TypeScript
    "types": "./types/index.d.ts",

    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs"
}

注意"types" 条件在 "exports" 中需要被放在开始的位置。

TypeScript 也支持 package.json 里的 "imports" 字段,它与查找声明文件的工作方式类似。 此外,还支持一个包引用它自己。 这些特性通常不特殊设置,但是是支持的。

设置模块检测策略

在 JavaScript 中引入模块带来的一个问题是让“Script”代码和新的模块代码之间的界限变得模糊。 (译者注:对于任意一段 JavaScript 代码,它的类型只能为 “Script” 或 “Module” 两者之一,它们是 ECMAScript 语言规范中定义的术语。) 模块中的 JavaScript 存在些许不同的执行方式和作用域规则,因此工具们需要确定每个文件的执行方式。 例如,Node.js 要求模块入口脚本是一个 .mjs 文件,或者它有一个邻近的 package.json 文件且带有 "type": "module"。 TypeScript 的规则则是如果一个文件里存在 importexport 语句,那么它是模块文件; 反之会把 .ts.js 文件当作是 “Script” 文件,它们存在于全局作用域

这与 Node.js 中对 package.json 的处理行为不同,因为 package.json 可以改变文件的类型;又或者是在 --jsx react-jsx 模式下一个 JSX 文件显式地导入了 JSX 工厂函数。 它也与当下的期望不符,因为大多数的 TypeScript 代码是基于模块来编写的。

以上就是 TypeScript 4.7 引入了 moduleDetection. moduleDetection 选项的原因。 它接受三个值:

  1. "auto",默认值
  2. "legacy",行为与 TypeScript 4.6 和以前的版本相同
  3. "force"

"auto" 模式下,TypeScript 不但会检测 importexport 语句,它还会检测:

  • 若启用了 --module nodenext / --module node16,那么 package.json 里的 "type" 字段是否为 "module",以及
  • 若启用了 --jsx react-jsx,那么当前文件是否为 JSX 文件。

在这些情况下,我们想将每个文件都当作模块文件。

"force" 选项能够保证每个非声明文件都被当成模块文件,不论 modulemoduleResolutonjsx 是如何设置的。

与此同时,使用 "legacy" 选项会回退到以前的行为,仅通过检测 importexport 语句来决定是否为模块文件。

更多详情请阅读PR

[] 语法元素访问的控制流分析

在 TypeScript 4.7 里,当索引键值是字面量类型和 unique symbol 类型时会细化访问元素的类型。 例如,有如下代码:

const key = Symbol();

const numberOrString = Math.random() < 0.5 ? 42 : "hello";

const obj = {
    [key]: numberOrString,
};

if (typeof obj[key] === "string") {
    let str = obj[key].toUpperCase();
}

在之前,TypeScript 不会处理涉及 obj[key] 的类型守卫,也就不知道 obj[key] 的类型是 string。 它会将 obj[key] 当作 string | number 类型,因此调用 toUpperCase() 会产生错误。

TypeScript 4.7 能够知道 obj[key] 的类型为 string

这意味着在 --strictPropertyInitialization 模式下,TypeScript 能够正确地检查计算属性是否被初始化。

// 'key' has type 'unique symbol'
const key = Symbol();

class C {
    [key]: string;

    constructor(str: string) {
        // oops, forgot to set 'this[key]'
    }

    screamString() {
        return this[key].toUpperCase();
    }
}

在 TypeScript 4.7 里,--strictPropertyInitialization 会提示错误说 [key] 属性在构造函数里没有被赋值。

感谢 Oleksandr Tarasiuk 提交的代码

改进对象和方法里的函数类型推断

TypeScript 4.7 可以对数组和对象里的函数进行更精细的类型推断。 它们可以像普通参数那样将类型从左向右进行传递。

declare function f<T>(arg: {
    produce: (n: string) => T,
    consume: (x: T) => void }
): void;

// Works
f({
    produce: () => "hello",
    consume: x => x.toLowerCase()
});

// Works
f({
    produce: (n: string) => n,
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    produce: n => n,
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    produce: function () { return "hello"; },
    consume: x => x.toLowerCase(),
});

// Was an error, now works.
f({
    produce() { return "hello" },
    consume: x => x.toLowerCase(),
});

之所以有些类型推断之前会失败是因为,若要知道 produce 函数的类型则需要在找到合适的类型 T 之前间接地获得 arg 的类型。 (译者注:这些之前失败的情况均是需要进行按上下文件归类的场景,即需要先知道 arg 的类型,才能确定 produce 的类型;如果不需要执行按上下文归类就能确定 produce 的类型则没有问题。) TypeScript 现在会收集与泛型参数 T 的类型推断相关的函数,然后进行惰性地类型推断。

更多详情请阅读这里

实例化表达式

我们偶尔可能会觉得某个函数过于通用了。 例如有一个 makeBox 函数。

interface Box<T> {
    value: T;
}

function makeBox<T>(value: T) {
    return { value };
}

假如我们想要定义一组更具体的可以收纳扳手锤子Box 函数。 为此,我们将 makeBox 函数包装进另一个函数,或者明确地定义一个 makeBox 的类型别名。

function makeHammerBox(hammer: Hammer) {
    return makeBox(hammer);
}

// 或者

const makeWrenchBox: (wrench: Wrench) => Box<Wrench> = makeBox;

这样可以工作,但有些浪费且笨重。 理想情况下,我们可以在替换泛型参数的时候直接声明 makeBox 的别名。

TypeScript 4.7 支持了该特性! 我们现在可以直接为函数和构造函数传入类型参数。

const makeHammerBox = makeBox<Hammer>;
const makeWrenchBox = makeBox<Wrench>;

这样我们可以让 makeBox 只接受更具体的类型并拒绝其它类型。

const makeStringBox = makeBox<string>;

// TypeScript 会提示错误
makeStringBox(42);

这对构造函数也生效,例如 ArrayMapSet

// 类型为 `new () => Map<string, Error>`
const ErrorMap = Map<string, Error>;

// 类型为 `Map<string, Error>`
const errorMap = new ErrorMap();

当函数或构造函数接收了一个类型参数,它会生成一个新的类型并保持所有签名使用了兼容的类型参数列表, 将形式类型参数替换成给定的实际类型参数。 其它种类的签名会被丢弃,因为 TypeScript 认为它们不会被使用到。

更多详情请阅读这里

infer 类型参数上的 extends 约束

有条件类型有点儿像一个进阶功能。 它允许我们匹配并依据类型结构进行推断,然后作出某种决定。 例如,编写一个有条件类型,它返回元组类型的第一个元素如果它类似 string 类型的话。

type FirstIfString<T> =
    T extends [infer S, ...unknown[]]
        ? S extends string ? S : never
        : never;

 // string
type A = FirstIfString<[string, number, number]>;

// "hello"
type B = FirstIfString<["hello", number, number]>;

// "hello" | "world"
type C = FirstIfString<["hello" | "world", boolean]>;

// never
type D = FirstIfString<[boolean, number, string]>;

FirstIfString 匹配至少有一个元素的元组类型,将元组第一个元素的类型提取到 S。 然后检查 Sstring 是否兼容,如果是就返回它。

可以注意到我们必须使用两个有条件类型来实现它。 我们也可以这样定义 FirstIfString

type FirstIfString<T> =
    T extends [string, ...unknown[]]
        // Grab the first type out of `T`
        ? T[0]
        : never;

它可以工作但要更多的“手动”操作且不够形象。 我们不是进行类型模式匹配并给首个元素命名,而是使用 T[0] 来提取 T 的第 0 个元素。 如果我们处理的是比元组类型复杂得多的类型就会变得棘手,因此 infer 可以让事情变得简单。

使用嵌套的条件来推断类型再去匹配推断出的类型是很常见的。 为了省去那一层嵌套,TypeScript 4.7 允许在 infer 上应用约束。

type FirstIfString<T> =
    T extends [infer S extends string, ...unknown[]]
        ? S
        : never;

通过这种方式,在 TypeScript 去匹配 S 时,它也会保证 Sstring 类型。 如果 S 不是 string 就是进入到 false 分支,此例中为 never

更多详情请阅读这里

可选的类型参数变型注释

先看一下如下的类型。

interface Animal {
    animalStuff: any;
}

interface Dog extends Animal {
    dogStuff: any;
}

// ...

type Getter<T> = () => T;

type Setter<T> = (value: T) => void;

假设有两个不同的 Getter 实例。 要想知道这两个 Getter 实例是否可以相互替换完全依赖于类型 T。 例如要知道 Getter<Dog> → Getter<Animal> 是否允许,则需要检查 Dog → Animal 是否允许。 因为对 TGetter<T> 的判断是相同“方向”的,我们称 Getter协变的。 相反的,判断 Setter<Dog> → Setter<Animal> 是否允许,需要检查 Animal → Dog 是否允许。 这种在方向上的“翻转”有点像数学里判断 $−x < −y$ 等同于判断 $y < x$。 当我们需要像这样翻转方向来比较 T 时,我们称 Setter 对于 T逆变的。

在 TypeScript 4.7 里,我们可以明确地声明类型参数上的变型关系。

因此,现在如果想在 Getter 上明确地声明对于 T 的协变关系则可以使用 out 修饰符。

type Getter<out T> = () => T;

相似的,如果想要明确地声明 Setter 对于 T 是逆变关系则可以指定 in 修饰符。

type Setter<in T> = (value: T) => void;

使用 outin 的原因是类型参数的变型关系依赖于它们被用在输出的位置还是输入的位置。 若不思考变型关系,你也可以只关注 T 是被用在输出还是输入位置上。

当然也有同时使用 outin 的时候。

interface State<in out T> {
    get: () => T;
    set: (value: T) => void;
}

T 被同时用在输入和输出的位置上时就成为了不变关系。 两个不同的 State<T> 不允许互换使用,除非两者的 T 是相同的。 换句话说,State<Dog>State<Animal> 不能互换使用。

从技术上讲,在纯粹的结构化类型系统里,类型参数和它们的变型关系不太重要 - 我们只需要将类型参数替换为实际类型,然后再比较相匹配的类型成员之间是否兼容。 那么如果 TypeScript 使用结构化类型系统为什么我们要在意类型参数的变型呢? 还有为什么我们会想要为它们添加类型注释呢?

其中一个原因是可以让读者能够明确地知道类型参数是如何被使用的。 对于十分复杂的类型来讲,可能很难确定一个类型参数是用于输入或者输出再或者两者兼有。 如果我们忘了说明类型参数是如何被使用的,TypeScript 也会提示我们。 举个例子,如果忘了在 State 上添加 inout 就会产生错误。

interface State<out T> {
    //          ~~~~~
    // error!
    // Type 'State<sub-T>' is not assignable to type 'State<super-T>' as implied by variance annotation.
    //   Types of property 'set' are incompatible.
    //     Type '(value: sub-T) => void' is not assignable to type '(value: super-T) => void'.
    //       Types of parameters 'value' and 'value' are incompatible.
    //         Type 'super-T' is not assignable to type 'sub-T'.
    get: () => T;
    set: (value: T) => void;
}

另一个原因则有关精度和速度。 TypeScript 已经在尝试推断类型参数的变型并做为一项优化。 这样做可以快速对大型的结构化类型进行类型检查。 提前计算变型省去了深入结构内部进行兼容性检查的步骤, 仅比较类型参数相比于一次又一次地比较完整的类型结构会快得多。 但经常也会出现这个计算十分耗时,并且在计算时产生了环,从而无法得到准确的变型关系。

type Foo<T> = {
    x: T;
    f: Bar<T>;
}

type Bar<U> = (x: Baz<U[]>) => void;

type Baz<V> = {
    value: Foo<V[]>;
}

declare let foo1: Foo<unknown>;
declare let foo2: Foo<string>;

foo1 = foo2;  // Should be an error but isn't ❌
foo2 = foo1;  // Error - correct ✅

提供明确的类型注解能够加快对环状类型的解析速度,有利于提高准确度。 例如,将上例的 T 设置为逆变可以帮助阻止有问题的赋值运算。

- type Foo<T> = {
+ type Foo<in out T> = {
      x: T;
      f: Bar<T>;
  }

我们并不推荐为所有的类型参数都添加变型注解; 例如,我们是能够(但不推荐)将变型设置为更严格的关系(即便实际上不需要), 因此 TypeScript 不会阻止你将类型参数设置为不变,就算它们实际上是协变的、逆变的或者是分离的。 因此,如果你选择添加明确的变型标记,我们推荐要经过深思熟虑后准确地使用它们。

但如果你操作的是深层次的递归类型,尤其是作为代码库作者,那么你可能会对使用这些注解来让用户获利感兴趣。 这些注解能够帮助提高准确性和类型检查速度,甚至可以增强代码编辑的体验。 可以通过实验来确定变型计算是否为类型检查时间的瓶颈,例如使用像 analyze-trace 这样的工具。

更多详情请阅读这里

使用 moduleSuffixes 自定义解析策略

TypeScript 4.7 支持了 moduleSuffixes 选项来自定义模块说明符的查找方式。

{
    "compilerOptions": {
        "moduleSuffixes": [".ios", ".native", ""]
    }
}

对于上述配置,如果有如下的导入语句:

import * as foo from "./foo";

它会尝试查找文件 ./foo.ios.ts./foo.native.ts 最后是 ./foo.ts

注意 moduleSuffixes 末尾的空字符串 "" 是必须的,只有这样 TypeScript 才会去查找 ./foo.ts。 也就是说,moduleSuffixes 的默认值是 [""]

这个功能对于 React Native 工程是很有用的,因为对于不同的目标平台会有不同的 tsconfig.jsonmoduleSuffixes

这个功能是由 Adam Foxman 贡献的!

resolution-mode

Node.js 的 ECMAScript 解析规则是根据当前文件所属的模式以及使用的语法来决定如何解析导入; 然而,在 ECMAScript 模块里引用 CommonJS 模块也是很常用的,或者反过来。

TypeScript 允许使用 /// <reference types="..." /> 指令。

/// <reference types="pkg" resolution-mode="require" />

// or

/// <reference types="pkg" resolution-mode="import" />

此外,在 Nightly 版本的 TypeScript 里,import type 可以指定导入断言来达到同样的目的。

// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from "pkg" assert {
    "resolution-mode": "require"
};

// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from "pkg" assert {
    "resolution-mode": "import"
};

export interface MergedType extends TypeFromRequire, TypeFromImport {}

这些断言也可以用在 import() 类型上。

export type TypeFromRequire =
    import("pkg", { assert: { "resolution-mode": "require" } }).TypeFromRequire;

export type TypeFromImport =
    import("pkg", { assert: { "resolution-mode": "import" } }).TypeFromImport;

export interface MergedType extends TypeFromRequire, TypeFromImport {}

import typeimport() 语法仅在 Nightly 版本里支持 resolution-mode。 你可能会看到如下的错误:

Resolution mode assertions are unstable.
Use nightly TypeScript to silence this error.
Try updating with 'npm install -D typescript@next'.

如果你在 TypeScript 的 Nightly 版本中使用了该功能,别忘了可以提供反馈

更多详情请查看 PR: 引用指令PR: 类型导入断言

跳转到在源码中的定义

TypeScript 4.7 支持了一个实验性的编辑器功能叫作 Go To Source Definition (跳转到在源码中的定义)。 它和 Go To Definition (跳转到定义)相似,但不是跳转到声明文件中。 而是查找相应的实现文件(比如 .js.ts 文件),并且在那里查找定义 - 即便这些文件总是会被声明文件 .d.ts 所遮蔽。

当你想查看导入的三方库的函数实现而不是 .d.ts 声明文件时是很便利的。

你可以在最新版本的 Visual Studio Code 里试用该功能。 但该功能还是预览版,存在一些已知的限制。 在某些情况下 TypeScript 使用启发式的方法来猜测函数定义的代码在哪个 .js 文件中, 因此结果可能不太精确。 Visual Studio Code 也不会提示哪些结果是通过猜测得到的,但我们正在实现它。

更多详情请参考 PR

分组整理导入语句

TypeScript 为 JavaScript 和 TypeScript 提供了叫做 “Organize Imports” (整理导入语句)编辑器功能。 可是,它的行为有点简单粗暴,它直接排序所有的导入语句。

例如,在如下的代码上使用 “Organize Imports”:

// local code
import * as bbb from "./bbb";
import * as ccc from "./ccc";
import * as aaa from "./aaa";

// built-ins
import * as path from "path";
import * as child_process from "child_process"
import * as fs from "fs";

// some code...

你会得到:

// local code
import * as child_process from "child_process";
import * as fs from "fs";
// built-ins
import * as path from "path";
import * as aaa from "./aaa";
import * as bbb from "./bbb";
import * as ccc from "./ccc";


// some code...

这不是我们想要的。 尽管导入语句已经按它们的路径排序了,并且注释和折行被保留了, 但仍不是我们期望的。

TypeScript 4.7 在 “Organize Imports” 时会考虑分组。 再次在上例代码上执行 “Organize Imports” 会得到期望的结果:

// local code
import * as aaa from "./aaa";
import * as bbb from "./bbb";
import * as ccc from "./ccc";

// built-ins
import * as child_process from "child_process";
import * as fs from "fs";
import * as path from "path";

// some code...

感谢 Minh QuyPR

TypeScript 4.6

允许在构造函数中的 super() 调用之前插入代码

在 JavaScript 的类中,在引用 this 之前必须先调用 super()。 在 TypeScript 中同样有这个限制,只不过在检查时过于严格。 在之前版本的 TypeScript 中,如果类中存在属性初始化器, 那么在构造函数里,在 super() 调用之前不允许出现任何其它代码。

class Base {
    // ...
}

class Derived extends Base {
    someProperty = true;

    constructor() {
        // 错误!
        // 必须先调用 'super()' 因为需要初始化 'someProperty'。
        doSomeStuff();
        super();
    }
}

这样做是因为程序实现起来容易,但这样做也会拒绝很多合法的代码。 TypeScript 4.6 放宽了限制,它允许在 super() 之前出现其它代码, 与此同时仍然会检查在引用 this 之前顶层的super() 已经被调用。

感谢 Joshua GoldbergPR

基于控制流来分析解构的可辨识联合类型

TypeScript 可以根据判别式属性来细化类型。 例如,在下面的代码中,TypeScript 能够在检查 kind 的类型后细化 action 的类型。

type Action =
    | { kind: "NumberContents", payload: number }
    | { kind: "StringContents", payload: string };

function processAction(action: Action) {
    if (action.kind === "NumberContents") {
        // `action.payload` is a number here.
        let num = action.payload * 2
        // ...
    }
    else if (action.kind === "StringContents") {
        // `action.payload` is a string here.
        const str = action.payload.trim();
        // ...
    }
}

这样就可以使用持有不同数据的对象,但通过共同的字段来区分它们。

这在 TypeScript 是很常见的;然而,根据个人的喜好,你可能想对上例中的 kindpayload 进行解构。 就像下面这样:

type Action =
    | { kind: "NumberContents", payload: number }
    | { kind: "StringContents", payload: string };

function processAction(action: Action) {
    const { kind, payload } = action;
    if (kind === "NumberContents") {
        let num = payload * 2
        // ...
    }
    else if (kind === "StringContents") {
        const str = payload.trim();
        // ...
    }
}

此前,TypeScript 会报错 - 当 kindpayload 是由同一个对象解构为变量时,它们会被独立对待。

在 TypeScript 4.6 中可以正常工作!

当解构独立的属性为 const 声明,或当解构参数到变量且没有重新赋值时,TypeScript 会检查被解构的类型是否为可辨识联合。 如果是的话,TypeScript 就能够根据类型检查来细化变量的类型。 因此上例中,通过检查 kind 的类型可以细化 payload 的类型。

更多详情请查看 PR

改进的递归深度检查

TypeScript 要面对一些有趣的挑战,因为它是构建在结构化类型系统之上,同时又支持了泛型。

在结构化类型系统中,对象类型的兼容性是由对象包含的成员决定的。

interface Source {
    prop: string;
}

interface Target {
    prop: number;
}

function check(source: Source, target: Target) {
    target = source;
    // error!
    // Type 'Source' is not assignable to type 'Target'.
    //   Types of property 'prop' are incompatible.
    //     Type 'string' is not assignable to type 'number'.
}

SourceTarget 的兼容性取决于它们的属性是否可以执行赋值操作。 此例中是指 prop 属性。

当引入了泛型后,有一些难题需要解决。 例如,下例中的 Source<string> 是否可以赋值给 Target<number>

interface Source<T> {
    prop: Source<Source<T>>;
}

interface Target<T> {
    prop: Target<Target<T>>;
}

function check(source: Source<string>, target: Target<number>) {
    target = source;
}

要想回答这个问题,TypeScript 需要检查 prop 的类型是否兼容。 这又要回答另一个问题:Source<Source<string>> 是否能够赋值给 Target<Target<number>>? 要想回答这个问题,TypeScript 需要检查 prop 的类型是否与那些类型兼容, 结果就是还要检查 Source<Source<Source<string>>> 是否能够赋值给 Target<Target<Target<number>>>? 继续发展下去,就会注意到类型会进行无限展开。

TypeScript 使用了启发式的算法 - 当一个类型达到特定的检查深度时,它表现出了将会进行无限展开, 那么就认为它可能是兼容的。 通常情况下这是没问题的,但是也可能出现漏报的情况。

interface Foo<T> {
    prop: T;
}

declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;
declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;

x = y;

通过人眼观察我们知道上例中的 xy 是不兼容的。 虽然类型的嵌套层次很深,但人家就是这样声明的。 启发式算法要处理的是在探测类型过程中生成的深层次嵌套类型,而非程序员明确手写出的类型。

TypeScript 4.6 现在能够区分出这类情况,并且对上例进行正确的错误提示。 此外,由于不再担心会对明确书写的类型进行误报, TypeScript 能够更容易地判断类型的无限展开, 并且降低了类型兼容性检查的成本。 因此,像 DefinitelyTyped 上的 redux-immutablereact-lazylogyup 代码库,对它们的类型检查时间降低了 50%。

你可能已经体验过这个改动了,因为它被挑选合并到了 TypeScript 4.5.3 中, 但它仍然是 TypeScript 4.6 中值得关注的一个特性。 更多详情请阅读 PR

索引访问类型推断改进

TypeScript 现在能够正确地推断通过索引访问到另一个映射对象类型的类型。

interface TypeMap {
    "number": number;
    "string": string;
    "boolean": boolean;
}

type UnionRecord<P extends keyof TypeMap> = { [K in P]:
    {
        kind: K;
        v: TypeMap[K];
        f: (p: TypeMap[K]) => void;
    }
}[P];

function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
    record.f(record.v);
}

// 这个调用之前是有问题的,但现在没有问题
processRecord({
    kind: "string",
    v: "hello!",

    // 'val' 之前会隐式地获得类型 'string | number | boolean',
    // 但现在会正确地推断为类型 'string'。
    f: val => {
        console.log(val.toUpperCase());
    }
})

该模式已经被支持了并允许 TypeScript 判断 record.f(record.v) 调用是合理的, 但是在以前,processRecord 调用中对 val 的类型推断并不好。

TypeScript 4.6 改进了这个情况,因此在启用 processRecord 时不再需要使用类型断言。

更多详情请阅读 PR

对因变参数的控制流分析

函数签名可以声明为剩余参数且其类型可以为可辨识联合元组类型。

function func(...args: ["str", string] | ["num", number]) {
    // ...
}

这意味着 func 的实际参数完全依赖于第一个实际参数。 若第一个参数为字符串 "str" 时,则第二个参数为 string 类型。 若第一个参数为字符串 "num" 时,则第二个参数为 number 类型。

像这样 TypeScript 是由签名来推断函数类型时,TypeScript 能够根据依赖的参数来细化类型。

type Func = (...args: ["a", number] | ["b", string]) => void;

const f1: Func = (kind, payload) => {
    if (kind === "a") {
        payload.toFixed();  // 'payload' narrowed to 'number'
    }
    if (kind === "b") {
        payload.toUpperCase();  // 'payload' narrowed to 'string'
    }
};

f1("a", 42);
f1("b", "hello");

更多详情请阅读 PR

--target es2022

TypeScript 的 --target 编译选项现在支持使用 es2022。 这意味着像类字段这样的特性能够稳定地在输出结果中保留。 这也意味着像 Arrays 的上 at() 和 Object.hasOwn 方法 或者 new Error 时的 cause 选项 可以通过设置新的 --target 或者 --lib es2022 来使用。

感谢 Kagami Sascha Rosylight (saschanaz)实现

删除 react-jsx 中不必要的参数

在以前,当使用 --jsx react-jsx 来编译如下的代码时

export const el = <div>foo</div>;

TypeScript 会生成如下的 JavaScript 代码:

import { jsx as _jsx } from "react/jsx-runtime";
export const el = _jsx("div", { children: "foo" }, void 0);

末尾的 void 0 参数是没用的,删掉它会减小打包的体积。

感谢 https://github.com/a-tarasyukPR,TypeScript 4.6 会删除 void 0 参数。

JSDoc 命名建议

在 JSDoc 里,你可以用 @param 标签来文档化参数。

/**
 * @param x The first operand
 * @param y The second operand
 */
function add(x, y) {
    return x + y;
}

但是,如果这些注释已经过时了会发生什么?就比如,我们将 xy 重命名为 ab

/**
 * @param x {number} The first operand
 * @param y {number} The second operand
 */
function add(a, b) {
    return a + b;
}

在之前 TypeScript 仅会在对 JavaScript 文件执行类型检查时报告这个问题 - 通过 使用 checkJs 选项,或者在文件顶端添加 // @ts-check 注释。

现在,你能够在编译器中的 TypeScript 文件上看到类似的提示! TypeScript 现在会给出建议,如果函数签名中的参数名与 JSDoc 中的参数名不一致。

example

改动是由 Alexander Tarasyuk 提供的!

JavaScript 中更多的语法和绑定错误提示

TypeScript 将更多的语法和绑定错误检查应用到了 JavaScript 文件上。 如果你在 Visual Studio 或 Visual Studio Code 这样的编辑器中打开 JavaScript 文件时就会看到这些新的错误提示, 或者当你使用 TypeScript 编译器来处理 JavaScript 文件时 - 即便你没有打开 checkJs 或者添加 // @ts-check 注释。

做为例子,如果在 JavaScript 文件中的同一个作用域中有两个同名的 const 声明, 那么 TypeScript 会报告一个错误。

const foo = 1234;
//    ~~~
// error: Cannot redeclare block-scoped variable 'foo'.

// ...

const foo = 5678;
//    ~~~
// error: Cannot redeclare block-scoped variable 'foo'.

另外一个例子,TypeScript 会报告修饰符是否被正确地使用了。

function container() {
    export function foo() {
//  ~~~~~~
// error: Modifiers cannot appear here.

    }
}

这些检查可以通过在文件顶端添加 // @ts-nocheck 注释来禁用, 但是我们很想听听在大家的 JavaScript 工作流中使用该特性的反馈。 你可以在 Visual Studio Code 安装 TypeScript 和 JavaScript Nightly 扩展 来提前体验, 并阅读 PR1PR1

TypeScript Trace 分析器

有人偶尔会遇到创建和比较类型时很耗时的情况。 TypeScript 提供了一个 --generateTrace 选项来帮助识别耗时的类型, 或者帮助诊断 TypeScript 编译器中的问题。 虽说由 --generateTrace 生成的信息是非常有帮助的(尤其是在 TypeScript 4.6 的改进后), 但是阅读这些 trace 信息是比较难的。

近期,我们发布了 @typescript/analyze-trace 工具来帮助阅读这些信息。 虽说我们不认为每个人都需要使用 analyze-trace,但是我们认为它会为遇到了 TypeScript 构建性能问题的团队提供帮助。

更多详情请查看 repo

TypeScript 4.5

支持从 node_modules 里读取 lib

为确保对 TypeScript 和 JavaScript 的支持可以开箱即用,TypeScript 内置了一些声明文件(.d.ts)。 这些声明文件描述了 JavaScript 语言中可用的 API,以及标准的浏览器 DOM API。 虽说 TypeScript 会根据工程中 target 的设置来提供默认值,但你仍然可以通过在 tsconfig.json 文件中设置 lib 来指定包含哪些声明文件。

TypeScript 包含的声明文件偶尔也会成为缺点:

  • 在升级 TypeScript 时,你必须要处理 TypeScript 内置声明文件的升级带来的改变,这可能成为一项挑战,因为 DOM API 的变动十分频繁。
  • 难以根据你的需求以及工程依赖的需求去定制声明文件(例如,工程依赖声明了需要使用 DOM API,那么你可能也必须要使用 DOM API)。

TypeScript 4.5 引入了覆盖特定内置 lib 的方式,它与 @types/ 的工作方式类似。 在决定应包含哪些 lib 文件时,TypeScript 会先去检查 node_modules 下面的 @typescript/lib-* 包。 例如,若将 dom 作为 lib 中的一项,那么 TypeScript 会尝试使用 node_modules/@typescript/lib-dom

然后,你就可以使用包管理器去安装特定的包作为 lib 中的某一项。 例如,现在 TypeScript 会将 DOM API 发布到 @types/web。 如果你想要给工程指定一个固定版本的 DOM API,你可以在 package.json 文件中添加如下代码:

{
  "dependencies": {
    "@typescript/lib-dom": "npm:@types/web"
  }
}

从 4.5 版本开始,你可以更新 TypeScript 和依赖管理工具生成的锁文件来确保使用固定版本的 DOM API。 你可以根据自己的情况来逐步更新类型声明。

十分感谢 saschanaz 提供的帮助。

更多详情,请参考 PR

改进 Awaited 类型和 Promise

TypeScript 4.5 引入了一个新的 Awaited 类型。 该类型用于描述 async 函数中的 await 操作,或者 Promise 上的 .then() 方法 - 尤其是递归地解开 Promise 的行为。

// A = string
type A = Awaited<Promise<string>>;

// B = number
type B = Awaited<Promise<Promise<number>>>;

// C = boolean | number
type C = Awaited<boolean | Promise<number>>;

Awaited 有助于描述现有 API,比如 JavaScript 内置的 Promise.allPromise.race 等等。 实际上,正是涉及 Promise.all 的类型推断问题促进了 Awaited 类型的产生。 例如,下例中的代码在 TypeScript 4.4 及之前的版本中会失败。

declare function MaybePromise<T>(value: T): T | Promise<T> | PromiseLike<T>;

async function doSomething(): Promise<[number, number]> {
  const result = await Promise.all([MaybePromise(100), MaybePromise(200)]);

  // 错误!
  //
  //    [number | Promise<100>, number | Promise<200>]
  //
  // 不能赋值给类型
  //
  //    [number, number]
  return result;
}

现在,Promise.all 结合并利用 Awaited 来提供更好的类型推断结果,同时上例中的代码也不再有错误。

更多详情,请参考 PR

模版字符串类型作为判别式属性

TypeScript 4.5 可以对模版字符串类型的值进行细化,同时可以识别模版字符串类型的判别式属性。

例如,下面的代码在以前会出错,但在 TypeScript 4.5 里没有错误。

export interface Success {
  type: `${string}Success`;
  body: string;
}

export interface Error {
  type: `${string}Error`;
  message: string;
}

export function handler(r: Success | Error) {
  if (r.type === "HttpSuccess") {
    // 'r' 的类型为 'Success'
    let token = r.body;
  }
}

更多详情,请参考 PR

module es2022

感谢 Kagami S. Rosylight,TypeScript 现在支持了一个新的 module 设置:es2022module es2022 的主要功能是支持顶层的 await,即可以在 async 函数外部使用 await。 该功能在 --module esnext 里已经被支持了(现在又增加了 --module nodenext),但 es2022 是支持该功能的首个稳定版本。

更多详情,请参考 PR

在条件类型上消除尾递归

当 TypeScript 检测到了以下情况时通常需要优雅地失败,比如无限递归、极其耗时以至影响编辑器使用体验的类型展开操作。 因此,TypeScript 会使用试探式的方法来确保它在试图拆分一个无限层级的类型时或操作将生成大量中间结果的类型时不会偏离轨道。

type InfiniteBox<T> = { item: InfiniteBox<T> };

type Unpack<T> = T extends { item: infer U } ? Unpack<U> : T;

// error: Type instantiation is excessively deep and possibly infinite.
type Test = Unpack<InfiniteBox<number>>;

上例是有意写成简单且没用的类型,但是存在大量有用的类型恰巧会触发试探。 作为示例,下面的 TrimLeft 类型会从字符串类型的开头删除空白。 若给定一个在开头位置有一个空格的字符串类型,它会直接将空格后面的字符串再传入 TrimLeft

type TrimLeft<T extends string> = T extends ` ${infer Rest}`
  ? TrimLeft<Rest>
  : T;

// Test = "hello" | "world"
type Test = TrimLeft<"   hello" | " world">;

这个类型也许有用,但如果字符串起始位置有 50 个空格,就会产生错误。

type TrimLeft<T extends string> = T extends ` ${infer Rest}`
  ? TrimLeft<Rest>
  : T;

// error: Type instantiation is excessively deep and possibly infinite.
type Test = TrimLeft<"                                                oops">;

这很讨厌,因为这种类型在表示字符串操作时很有用 - 例如,URL 路由解析器。 更差的是,越有用的类型越会创建更多的实例化类型,结果就是对输入参数会有限制。

但也有一个可取之处:TrimLeft 在一个分支中使用了尾递归的方式编写。 当它再次调用自己时,是直接返回了结果并且不存在后续操作。 由于这些类型不需要创建中间结果,因此可以被更快地实现并且可以避免触发 TypeScript 内置的类型递归试探。

这就是 TypeScript 4.5 在条件类型上删除尾递归的原因。 只要是条件类型的某个分支为另一个条件类型,TypeScript 就不会去生成中间类型。 虽说仍然会进行一些试探来确保类型没有偏离方向,但已无伤大雅。

注意,下面的类型不会被优化,因为它使用了包含条件类型的联合类型。

type GetChars<S> = S extends `${infer Char}${infer Rest}`
  ? Char | GetChars<Rest>
  : never;

如果你想将它改成尾递归,可以引入帮助类型来接收一个累加类型的参数,就如同尾递归函数一样。

type GetChars<S> = GetCharsHelper<S, never>;
type GetCharsHelper<S, Acc> = S extends `${infer Char}${infer Rest}`
  ? GetCharsHelper<Rest, Char | Acc>
  : Acc;

更多详情,请参考 PR

禁用导入省略

在某些情况下,TypeScript 无法检测导入是否被使用。 例如,考虑下面的代码:

import { Animal } from "./animal.js";

eval("console.log(new Animal().isDangerous())");

默认情况下,TypeScript 会删除上面的导入语句,因为它看上去没有被使用。 在 TypeScript 4.5 里,你可以启用新的标记 preserveValueImports 来阻止 TypeScript 从生成的 JavaScript 代码里删除导入的值。 虽说应该使用 eval 的理由不多,但在 Svelte 框架里有相似的情况:

<!-- A .svelte File -->
<script>
  import { someFunc } from "./some-module.js";
</script>

<button on:click="{someFunc}">Click me!</button>

同样在 Vue.js 中,使用 <script setup> 功能:

<!-- A .vue File -->
<script setup>
  import { someFunc } from "./some-module.js";
</script>

<button @click="someFunc">Click me!</button>

这些框架会根据 <script> 标签外的标记来生成代码,但 TypeScript 仅仅会考虑 <script> 标签内的代码。 也就是说 TypeScript 会自动删除对 someFunc 的导入,因此上面的代码无法运行! 使用 TypeScript 4.5,你可以通过 preserveValueImports 来避免发生这种情况。

当该标记和 --isolatedModules` 一起使用时有个额外要求:导入的类型必须被标记为 type-only,因为编译器一次处理一个文件,无法知道是否导入了未被使用的值,或是导入了必须要被删除的类型以防运行时崩溃。

// Which of these is a value that should be preserved? tsc knows, but `ts.transpileModule`,
// ts-loader, esbuild, etc. don't, so `isolatedModules` gives an error.
import { someFunc, BaseType } from "./some-module.js";
//                 ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// when 'preserveValueImports' and 'isolatedModules' are both enabled.

这催生了另一个 TypeScript 4.5 的功能,导入语句中的 type 修饰符,它尤其重要。

更多详情,请参考 PR

在导入名称前使用 type 修饰符

上面提到,preserveValueImportsisolatedModules 结合使用时有额外的要求,这是为了让构建工具能够明确知道是否可以省略导入语句。

// Which of these is a value that should be preserved? tsc knows, but `ts.transpileModule`,
// ts-loader, esbuild, etc. don't, so `isolatedModules` issues an error.
import { someFunc, BaseType } from "./some-module.js";
//                 ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// when 'preserveValueImports' and 'isolatedModules' are both enabled.

当同时使用了这些选项时,需要有一种方式来表示导入语句是否可以被合法地丢弃。 TypeScript 已经有类似的功能,即 import type

import type { BaseType } from "./some-module.js";
import { someFunc } from "./some-module.js";

export class Thing implements BaseType {
  // ...
}

这是有效的,但还可以提供更好的方式来避免使用两条导入语句从相同的模块中导入。 因此,TypeScript 4.5 允许在每个命名导入前使用 type 修饰符,你可以按需混合使用它们。

import { someFunc, type BaseType } from "./some-module.js";

export class Thing implements BaseType {
    someMethod() {
        someFunc();
    }
}

上例中,在 preserveValueImports 模式下,能够确定 BaseType 可以被删除,同时 someFunc 应该被保留,于是就会生成如下代码:

import { someFunc } from "./some-module.js";

export class Thing {
  someMethod() {
    someFunc();
  }
}

更多详情,请参考 PR

私有字段存在性检查

TypeScript 4.5 支持了检查对象上是否存在某私有字段的 ECMAScript Proposal。 现在,你可以编写带有 #private 字段成员的类,然后使用 in 运算符检查另一个对象是否包含相同的字段。

class Person {
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }

    equals(other: unknown) {
        return other &&
            typeof other === "object" &&
            #name in other && // <- this is new!
            this.#name === other.#name;
    }
}

该功能一个有趣的地方是,#name in other 隐含了 other 必须是使用 Person 构造的,因为只有在这种情况下才可能存在该字段。 这是该提议中关键的功能之一,同时也是为什么这项提议叫作 “ergonomic brand checks” 的原因 - 因为私有字段通常作为一种“商标”来区分不同类的实例。 因此,TypeScript 能够在每次检查中细化 other类型,直到细化为 Person 类型。

感谢来自 Bloomberg 的朋友提交的 PRAshley ClaymoreTitian Cernicova-DragomirKubilay Kahveci,和 Rob Palmer

导入断言

TypeScript 4.5 支持了 ECMAScript Proposal 中的 导入断言。 该语法会被运行时所使用来检查导入是否为期望的格式。

import obj from "./something.json" assert { type: "json" };

TypeScript 不会检查这些断言,因为它们依赖于宿主环境。 TypeScript 会保留原样,稍后让浏览器或者运行时来处理它们(也可能会出错)。

// TypeScript 允许
// 但浏览器可能不允许
import obj from "./something.json" assert {
    type: "fluffy bunny"
};

动态的 import() 调用可以通过第二个参数来使用导入断言。

const obj = await import("./something.json", {
  assert: { type: "json" },
});

第二个参数的类型为 ImportCallOptions,并且目前它只接受一个 assert 属性。

感谢 Wenlu Wang 实现了 这个功能

使用 realPathSync.native 获得更快的加载速度

TypeScript 在所有操作系统上使用了 Node.js realPathSync 函数的系统原生实现。

以前,这个函数只在 Linux 上使用了,但在 TypeScript 4.5 中,在大小写不敏感的操作系统上,如 Windows 和 MacOS,也被采用了。 对于一些代码库来讲这个改动会提升 5 ~ 13% 的加载速度(和操作系统有关)。

更多详情请参考 PR

JSX Attributes 的代码片段自动补全

TypeScript 4.5 为 JSX 属性提供了代码片段自动补全功能。 当在 JSX 标签上输入属性时,TypeScript 已经能够提供提供建议; 但对于代码片段自动补全来讲,它们会删除部分已经输入的字符来添加一个初始化器并将光标放到正确的位置。

Snippet completions for JSX attributes. For a string property, quotes are automatically added. For a numeric properties, braces are added.

TypeScript 通常会使用属性的类型来判断插入哪种初始化器,但你可以在 Visual Studio Code 中自定义该行为。

Settings in VS Code for JSX attribute completions

注意,该功能只在新版本的 Visual Studio Code 中支持,因此你可能需要使用 Insiders 版本。 更多详情,请参考 PR

为未解决类型提供更好的编辑器支持

在某些情况下,编辑器会使用一个轻量级的“部分”语义模式 - 比如编辑器正在等待加载完整的工程,又或者是 GitHub 的基于 web 的编辑器

在旧版本 TypeScript 中,如果语言服务无法找到一个类型,它会输出 any

Hovering over a signature where Buffer isn't found, TypeScript replaces it with any.

上例中,没有找到 Buffer,因此 TypeScript 在 quick info 里显示了 any。 在 TypeScript 4.5 中,TypeScript 会尽可能保留你编写的代码。

Hovering over a signature where Buffer isn't found, it continues to use the name Buffer.

然而,当你将鼠标停在 Buffer 上时,你会看到 TypeScript 无法找到 Buffer 的提示。

TypeScript displays type Buffer = /* unresolved */ any;

总之,在 TypeScript 还没有读取整个工程的时候,它提供了更加平滑的体验。 注意,在其它正常情况下,当无法找到某个类型时总会产生错误。

更多详情,请参考 PR

TypeScript 4.4

针对条件表达式和判别式的别名引用进行控制流分析

在 JavaScript 中,总会用多种方式对某个值进行检查,然后根据不同类型的值执行不同的操作。 TypeScript 能够理解这些检查,并将它们称作为类型守卫。 我们不需要在变量的每一个使用位置上都指明类型,TypeScript 的类型检查器能够利用基于控制流的分析技术来检查是否在前面使用了类型守卫。

例如,可以这样写

function foo(arg: unknown) {
    if (typeof arg === 'string') {
        console.log(arg.toUpperCase());
        //           ^?
    }
}

这个例子中,我们检查 arg 是否为 string 类型。 TypeScript 识别出了 typeof arg === "string" 检查,它被当作是一个类型守卫,并且知道在 if 分支内 arg 的类型为 string。 这样就可以正常地访问 string 类型上的方法,例如 toUpperCase()

但如果我们将条件表达式提取到一个名为 argIsString 的常量会发生什么?

// 在 TS 4.3 及以下版本

function foo(arg: unknown) {
    const argIsString = typeof arg === 'string';
    if (argIsString) {
        console.log(arg.toUpperCase());
        //              ~~~~~~~~~~~
        // 错误!'unknown' 类型上不存在 'toUpperCase' 属性。
    }
}

在之前版本的 TypeScript 中,这样做会产生错误 - 就算 argIsString 的值为类型守卫,TypeScript 也会丢掉这个信息。 这不是想要的结果,因为我们可能想要在不同的地方重用这个检查。 为了绕过这个问题,通常需要重复多次代码或使用类型断言。

在 TypeScript 4.4 中,情况有所改变。 上面的例子不再产生错误! 当 TypeScript 看到我们在检查一个常量时,会额外检查它是否包含类型守卫。 如果那个类型守卫操作的是 const 常量,某个 readonly 属性或某个未修改的参数,那么 TypeScript 能够对该值进行类型细化。

不同种类的类型守卫都支持,不只是 typeof 类型守卫。 例如,对于可辨识联合类型同样适用。

type Shape =
    | { kind: 'circle'; radius: number }
    | { kind: 'square'; sideLength: number };

function area(shape: Shape): number {
    const isCircle = shape.kind === 'circle';
    if (isCircle) {
        // 知道此处为 circle
        return Math.PI * shape.radius ** 2;
    } else {
        // 知道此处为 square
        return shape.sideLength ** 2;
    }
}

在 TypeScript 4.4 版本中对判别式的分析又进了一层 - 现在可以提取出判别式然后细化原来的对象类型。

type Shape =
    | { kind: 'circle'; radius: number }
    | { kind: 'square'; sideLength: number };

function area(shape: Shape): number {
    // Extract out the 'kind' field first.
    const { kind } = shape;

    if (kind === 'circle') {
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    } else {
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}

另一个例子,该函数会检查它的两个参数是否有内容。

function doSomeChecks(
    inputA: string | undefined,
    inputB: string | undefined,
    shouldDoExtraWork: boolean
) {
    const mustDoWork = inputA && inputB && shouldDoExtraWork;
    if (mustDoWork) {
        // We can access 'string' properties on both 'inputA' and 'inputB'!
        const upperA = inputA.toUpperCase();
        const upperB = inputB.toUpperCase();
        // ...
    }
}

TypeScript 知道如果 mustDoWorktrue 那么 inputAinputB 都存在。 也就是说不需要编写像 inputA! 这样的非空断言的代码来告诉 TypeScript inputA 不为 undefined

一个好的性质是该分析同时具有可传递性。 TypeScript 可以通过这些常量来理解在它们背后执行的检查。

function f(x: string | number | boolean) {
    const isString = typeof x === 'string';
    const isNumber = typeof x === 'number';
    const isStringOrNumber = isString || isNumber;
    if (isStringOrNumber) {
        x;
        //  ^?
    } else {
        x;
        //  ^?
    }
}

注意这里会有一个截点 - TypeScript 并不是毫无限制地去追溯检查这些条件表达式,但对于大多数使用场景而言已经足够了。

这个功能能让很多直观的 JavaScript 代码在 TypeScript 里也好用,而不会妨碍我们。 更多详情请参考 PR

Symbol 以及模版字符串索引签名

TypeScript 支持使用索引签名来为对象的每个属性定义类型。 这样我们就可以将对象当作字典类型来使用,把字符串放在方括号里来进行索引。

例如,可以编写由 string 类型的键映射到 boolean 值的类型。 如果我们给它赋予 boolean 类型以外的值会报错。

interface BooleanDictionary {
    [key: string]: boolean;
}

declare let myDict: BooleanDictionary;

// 允许赋予 boolean 类型的值
myDict['foo'] = true;
myDict['bar'] = false;

// 错误
myDict['baz'] = 'oops';

虽说在这里 Map 可能是更适合的数据结构(具体的说是 Map<string, boolean>),但 JavaScript 对象通常更方便或者正是我们要操作的目标。

相似地,Array<T> 已经定义了 number 索引签名,我们可以插入和获取 T 类型的值。

// 这是 TypeScript 内置的部分 Array 类型
interface Array<T> {
    [index: number]: T;

    // ...
}

let arr = new Array<string>();

// 没问题
arr[0] = 'hello!';

// 错误,期待一个 'string' 值
arr[1] = 123;

索引签名是一种非常有用的表达方式。 然而,直到现在它们只能使用 stringnumber 类型的键(string 索引签名存在一个有意为之的怪异行为,它们可以接受 number 类型的键,因为 number 会被转换为字符串)。 这意味着 TypeScript 不允许使用 symbol 类型的键来索引对象。 TypeScript 也无法表示由一部分 string 类型的键组成的索引签名 - 例如,对象属性名是以 data- 字符串开头的索引签名。

TypeScript 4.4 解决了这个问题,允许 symbol 索引签名以及模版字符串。

例如,TypeScript 允许声明一个接受任意 symbol 值作为键的对象类型。

interface Colors {
    [sym: symbol]: number;
}

const red = Symbol('red');
const green = Symbol('green');
const blue = Symbol('blue');

let colors: Colors = {};

// 没问题
colors[red] = 255;
let redVal = colors[red];
//  ^ number

colors[blue] = 'da ba dee';
// 错误:'string' 不能赋值给 'number'

相似地,可以定义带有模版字符串的索引签名。 一个场景是用来免除对以 data- 开头的属性名执行的 TypeScript 额外属性检查。 当传递一个对象字面量给目标类型时,TypeScript 会检查是否存在相比于目标类型的额外属性。

interface Options {
    width?: number;
    height?: number;
}

let a: Options = {
    width: 100,
    height: 100,

    'data-blah': true,
};

interface OptionsWithDataProps extends Options {
    // 允许以 'data-' 开头的属性
    [optName: `data-${string}`]: unknown;
}

let b: OptionsWithDataProps = {
    width: 100,
    height: 100,
    'data-blah': true,

    // 使用未知属性会报错,不包括以 'data-' 开始的属性
    'unknown-property': true,
};

最后,索引签名现在支持联合类型,只要它们是无限域原始类型的联合 - 尤其是:

  • string
  • number
  • symbol
  • 模版字符串(例如 `hello-${string}`

带有以上类型的联合的索引签名会展开为不同的索引签名。

interface Data {
    [optName: string | symbol]: any;
}

// 等同于

interface Data {
    [optName: string]: any;
    [optName: symbol]: any;
}

更多详情请参考 PR

Defaulting to the unknown Type in Catch Variables (--useUnknownInCatchVariables)

异常捕获变量的类型默认为 unknown--useUnknownInCatchVariables

在 JavaScript 中,允许使用 throw 语句抛出任意类型的值,并在 catch 语句中捕获它。 因此,TypeScript 从前会将异常捕获变量的类型设置为 any 类型,并且不允许指定其它的类型注解:

try {
    // 谁知道它会抛出什么东西
    executeSomeThirdPartyCode();
} catch (err) {
    // err: any
    console.error(err.message); // 可以,因为类型为 'any'
    err.thisWillProbablyFail(); // 可以,因为类型为 'any' :(
}

当 TypeScript 引入了 unknown 类型后,对于追求高度准确性和类型安全的用户来讲在 catch 语句的捕获变量处使用 unknown 成为了比 any 类型更好的选择,因为它强制我们去检测要使用的值。 后来,TypeScript 4.0 允许用户在 catch 语句中明确地指定 unknown(或 any)类型,这样就可以根据情况有选择一使用更严格的类型检查; 然而,在每一处 catch 语句里手动指定 : unknown 是一件繁琐的事情。

因此,TypeScript 4.4 引入了一个新的标记 --useUnknownInCatchVariables。 它将 catch 语句捕获变量的默认类型由 any 改为 unknown

declare function executeSomeThirdPartyCode(): void;

try {
    executeSomeThirdPartyCode();
} catch (err) {
    // err: unknown

    // Error! Property 'message' does not exist on type 'unknown'.
    console.error(err.message);

    // Works! We can narrow 'err' from 'unknown' to 'Error'.
    if (err instanceof Error) {
        console.error(err.message);
    }
}

这个标记属性于 --strict 标记家族的一员。 也就是说如果你启用了 --strict,那么该标记也自动启用了。 在 TypeScript 4.4 中,你可能会看到如下的错误:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

如果我们不想处理 catch 语句中 unknown 类型的捕获变量,那么可以明确使用 : any 类型注解,这样就会关闭严格类型检查。

declare function executeSomeThirdPartyCode(): void;

try {
    executeSomeThirdPartyCode();
} catch (err: any) {
    console.error(err.message); // Works again!
}

更多详情请参考 PR

确切的可选属性类型 (--exactOptionalPropertyTypes)

在 JavaScript 中,读取对象上某个不存在的属性会得到 undefined 值。 与此同时,某个已有属性的值也允许为 undefined 值。 有许多 JavaScript 代码都会对这些情况一视同仁,因此最初 TypeScript 将可选属性视为添加了 undefined 类型。 例如,

interface Person {
    name: string;
    age?: number;
}

等同于:

interface Person {
    name: string;
    age?: number | undefined;
}

这意味着用户可以给 age 明确地指定 undefined 值。

const p: Person = {
    name: 'Daniel',
    age: undefined, // This is okay by default.
};

因此默认情况下,TypeScript 不区分带有 undefined 类型的属性和不存在的属性。 虽说这在大部分情况下是没问题的,但并非所有的 JavaScript 代码都如此。 像是 Object.assignObject.keys,对象展开({ ...obj })和 for-in 循环这样的函数和运算符会区别对待属性是否存在于对象之上。 在 Person 例子中,如果 age 属性的存在与否是至关重要的,那么就可能会导致运行时错误。

在 TypeScript 4.4 中,新的 --exactOptionalPropertyTypes 标记指明了可选属性的确切表示方式,即不自动添加 | undefined 类型:

interface Person {
    name: string;
    age?: number;
}

// 启用 'exactOptionalPropertyTypes'
const p: Person = {
    name: 'Daniel',
    age: undefined, // 错误!undefined 不是一个成员
};

该标记不是 --strict 标记家族的一员,需要显式地开启。 该标记要求同时启用 --strictNullChecks 标记。 我们已经更新了 DefinitelyTyped 以及其它的声明定义来帮助进行平稳地过渡,但你仍可能遇到一些问题,这取决于代码的结构。

更多详情请参考 PR

类中的 static 语句块

TypeScript 4.4 支持了 类中的 static 语句块,一个即将到来的 ECMAScript 特性,它能够帮助编写复杂的静态成员初始化代码。

declare function someCondition(): boolean

class Foo {
    static count = 0;

    // 静态语句块:
    static {
        if (someCondition()) {
            Foo.count++;
        }
    }
}

在静态语句块中允许编写一系列语句,它们可以访问类中的私有字段。 也就是说在初始化代码中能够编写语句,不会暴露变量,并且可以完全访问类的内部信息。

declare function loadLastInstances(): any[]

class Foo {
    static #count = 0;

    get count() {
        return Foo.#count;
    }

    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

若不使用 static 语句块也能够编写上述代码,只不过需要使用一些折中的 hack 手段。

一个类可以有多个 static 语句块,它们的运行顺序与编写顺序一致。

// Prints:
//    1
//    2
//    3
class Foo {
    static prop = 1
    static {
        console.log(Foo.prop++);
    }
    static {
        console.log(Foo.prop++);
    }
    static {
        console.log(Foo.prop++);
    }
}

感谢 Wenlu Wang 为 TypeScript 添加了该支持。 更多详情请参考 PR

tsc --help 更新与优化

TypeScript 的 --help 选项完全更新了! 感谢 Song Gao,我们更新了编译选项的描述--help 菜单的配色样式

The new TypeScript --help menu where the output is bucketed into several different areas

更多详情请参考 Issue

性能优化

更快地生成声明文件

TypeScript 现在会缓存下内部符号是否可以在不同上下文中被访问,以及如何显示指定的类型。 这些改变能够改进 TypeScript 处理复杂类型时的性能,尤其是在使用了 --declaration 标记来生成 .d.ts 文件的时候。

更多详情请参考 PR

更快地标准化路径

TypeScript 经常需要对文件路径进行“标准化”操作来得到统一的格式,以便编译器能够随处使用它。 它包括将反斜线替换成正斜线,或者删除路径中间的 /.//../ 片段。 当 TypeScript 需要处理成千上万的路径时,这个操作就会很慢。 在 TypeScript 4.4 里会先对路径进行快速检查,判断它们是否需要进行标准化。 这些改进能够减少 5-10% 的工程加载时间,对于大型工程来讲效果会更加明显。

更多详情请参考 PR 以及 PR

更快地路径映射

TypeScript 现在会缓存构造的路径映射(通过 tsconfig.json 里的 paths)。 对于拥有数百个路径映射的工程来讲效果十分明显。 更多详情请参考 PR

更快地增量构建与 --strict

这曾是一个缺陷,在 --incremental 模式下,如果启用了 --strict 则 TypeScript 会重新进行类型检查。 这导致了不管是否开启了 --incremental 构建速度都挺慢。 TypeScript 4.4 修复了这个问题,该修复也应用到了 TypeScript 4.3 里。

更多详情请参考 PR

针对大型输出更快地生成 Source Map

TypeScript 4.4 优化了为超大输出文件生成 source map 的速度。 在构建旧版本的 TypeScript 编译器时,结果显示节省了 8% 的生成时间。

感谢 David Michon 提供了这项简洁的优化

更快的 --force 构建

当在工程引用上使用了 --build 模式时,TypeScript 必须执行“是否更新检查”来确定是否需要重新构建。 在进行 --force 构建时,该检查是无关的,因为每个工程依赖都要被重新构建。 在 TypeScript 4.4 里,--force 会避免执行无用的步骤并进行完整的构建。 更多详情请参考 PR

JavaScript 中的拼写建议

TypeScript 为在 Visual Studio 和 Visual Studio Code 等编辑器中的 JavaScript 编写体验赋能。 大多数情况下,在处理 JavaScript 文件时,TypeScript 会置身事外; 然而,TypeScript 经常能够提供有理有据的建议且不过分地侵入其中。

这就是为什么 TypeScript 会为 JavaScript 文件提供拼写建议 - 不带有 // @ts-check 的 文件或者关闭了 checkJs 选项的工程。 即,TypeScript 文件中已有的 "Did you mean...?" 建议,现在它们也作用于 JavaScript 文件。

这些拼写建议也暗示了代码中可能存在错误。 我们在测试该特性时已经发现了已有代码中的一些错误!

更多详情请参考 PR

内嵌提示(Inlay Hints)

TypeScript 4.4 支持了内嵌提示特性,它能帮助显示参数名和返回值类型等信息。 可将其视为一种友好的“ghost text”。

A preview of inlay hints in Visual Studio Code

该特性由 Wenlu WangPR 所实现。

他也在 Visual Studio Code 里进行了集成 并在 July 2021 (1.59) 发布。 若你想尝试该特性,需确保安装了稳定版insiders 版本的编辑器。 你也可以在 Visual Studio Code 的设置里修改何时何地显示内嵌提示。

自动导入的补全列表里显示真正的路径

当 Visual Studio Code 显示补全列表时,包含自动导入在内的补全列表里会显示指向模块的路径; 然而,该路径通常不是 TypeScript 最终替换进来的模块描述符。 该路径通常是相对于 workspace 的,如果你导入了 moment 包,你大概会看到 node_modules/moment 这样的路径 。

A completion list containing unwieldy paths containing 'node_modules'. For example, the label for 'calendarFormat' is 'node_modules/moment/moment' instead of 'moment'.

这些路径很难处理且容易产生误导,尤其是插入的路径同时需要考虑 Node.js 的 node_modules 解析,路径映射,符号链接以及重新导出等。

这就是为什么 TypeScript 4.4 中的补全列表会显示真正的导入模块路径。

A completion list containing clean paths with no intermediate 'node_modules'. For example, the label for 'calendarFormat' is 'moment' instead of 'node_modules/moment/moment'.

由于该计算可能很昂贵,当补全列表包含许多条目时最终的模块描述符会在你输入更多的字符时显示出来。 你仍可能看到基于 workspace 的相对路径;然而,当编辑器“预热”后,再多输入几个字符它们会被替换为真正的路径。

TypeScript 4.3

拆分属性的写入类型

在 JavaScript 中,API 经常需要对传入的值进行转换,然后再保存。 这种情况在 getter 和 setter 中也常出现。 例如,在某个类中的一个 setter 总是需要将传入的值转换成 number,然后再保存到私有字段中。

class Thing {
    #size = 0;

    get size() {
        return this.#size;
    }
    set size(value) {
        let num = Number(value);

        // Don't allow NaN and stuff.
        if (!Number.isFinite(num)) {
            this.#size = 0;
            return;
        }

        this.#size = num;
    }
}

我们该如何将这段 JavaScript 代码改写为 TypeScript 呢? 从技术上讲,我们不必进行任何特殊处理 - TypeScript 能够识别出 size 是一个数字。

但问题在于 size 不仅仅是允许将 number 赋值给它。 我们可以通过将 size 声明为 unknownany 来解决这个问题:

class Thing {
    // ...
    get size(): unknown {
        return this.#size;
    }
}

但这不太友好 - unknown 类型会强制在读取 size 值时进行类型断言,同时 any 类型也不会去捕获错误。 如果我们真想要为转换值的 API 进行建模,那么之前版本的 TypeScript 会强制我们在准确性(读取容易,写入难)和自由度(写入方便,读取难)两者之间进行选择。

这就是 TypeScript 4.3 允许分别为读取和写入属性值添加类型的原因。

class Thing {
    #size = 0;

    get size(): number {
        return this.#size;
    }

    set size(value: string | number | boolean) {
        let num = Number(value);

        // Don't allow NaN and stuff.
        if (!Number.isFinite(num)) {
            this.#size = 0;
            return;
        }

        this.#size = num;
    }
}

上例中,set 存取器使用了更广泛的类型种类(stringbooleannumber),但 get 存取器保证它的值为number。 现在,我们再给这类属性赋予其它类型的值就不会报错了!

class Thing {
    #size = 0;

    get size(): number {
        return this.#size;
    }

    set size(value: string | number | boolean) {
        let num = Number(value);

        // Don't allow NaN and stuff.
        if (!Number.isFinite(num)) {
            this.#size = 0;
            return;
        }

        this.#size = num;
    }
}
// ---cut---
let thing = new Thing();

// 可以给 `thing.size` 赋予其它类型的值!
thing.size = 'hello';
thing.size = true;
thing.size = 42;

// 读取 `thing.size` 总是返回数字!
let mySize: number = thing.size;

当需要判定两个同名属性间的关系时,TypeScript 将只考虑“读取的”类型(比如,get 存取器上的类型)。 而“写入”类型只在直接写入属性值时才会考虑。

注意,这个模式不仅作用于类。 你也可以在对象字面量中为 getter 和 setter 指定不同的类型。

function makeThing(): Thing {
    let size = 0;
    return {
        get size(): number {
            return size;
        },
        set size(value: string | number | boolean) {
            let num = Number(value);

            // Don't allow NaN and stuff.
            if (!Number.isFinite(num)) {
                size = 0;
                return;
            }

            size = num;
        },
    };
}

事实上,我们在接口/对象类型上支持了为属性的读和写指定不同的类型。

// Now valid!
interface Thing {
    get size(): number;
    set size(value: number | string | boolean);
}

此处的一个限制是属性的读取类型必须能够赋值给属性的写入类型。 换句话说,getter 的类型必须能够赋值给 setter。 这在一定程度上确保了一致性,一个属性应该总是能够赋值给它自身。

更多详情,请参考PR

override--noImplicitOverride 标记

当在 JavaScript 中去继承一个类时,覆写方法十分容易 - 但不幸的是可能会犯一些错误。

其中一个就是会导致丢失重命名。 例如:

class SomeComponent {
    show() {
        // ...
    }
    hide() {
        // ...
    }
}

class SpecializedComponent extends SomeComponent {
    show() {
        // ...
    }
    hide() {
        // ...
    }
}

SpecializedComponentSomeComponent 的子类,并且覆写了 showhide 方法。 猜一猜,如果有人想要将 showhide 方法删除并用单个方法代替会发生什么?

 class SomeComponent {
-    show() {
-        // ...
-    }
-    hide() {
-        // ...
-    }
+    setVisible(value: boolean) {
+        // ...
+    }
 }
 class SpecializedComponent extends SomeComponent {
     show() {
         // ...
     }
     hide() {
         // ...
     }
 }

哦,不! SpecializedComponent 中的方法没有被更新。 而是变为添加了两个没用的 showhide 方法,它们可能都没有被调用。

此处的部分问题在于我们不清楚这里是想添加新的方法,还是想覆写已有的方法。 因此,TypeScript 4.3 增加了 override 关键字。

class SpecializedComponent extends SomeComponent {
    override show() {
        // ...
    }
    override hide() {
        // ...
    }
}

当一个方法被标记为 override,TypeScript 会确保在基类中存在同名的方法。

class SomeComponent {
    setVisible(value: boolean) {
        // ...
    }
}
class SpecializedComponent extends SomeComponent {
    override show() {
        //   ~~~~
        //   错误
    }
}

这是一项重大改进,但如果忘记在方法前添加 override 则不会起作用 - 这也是人们常犯的错误。

例如,可能会不小心覆写了基类中的方法,并且还没有意识到。

class Base {
    someHelperMethod() {
        // ...
    }
}

class Derived extends Base {
    // 不是真正想覆写基类中的方法,
    // 只是想编写一个本地的帮助方法
    someHelperMethod() {
        // ...
    }
}

因此,TypeScript 4.3 中还增加了一个 --noImplicitOverride 选项。 当启用了该选项,如果覆写了父类中的方法但没有添加 override 关键字,则会产生错误。 在上例中,如果启用了 --noImplicitOverride,则 TypeScript 会报错,并提示我们需要重命名 Derived 中的方法。

感谢开发者社区的贡献。 该功能是在这个 PR中由Wenlu Wang实现,一个更早的 override 实现是由Paul Cody Johnston完成。

模版字符串类型改进

在近期的版本中,TypeScript 引入了一种新类型,即:模版字符串类型。 它可以通过连接操作来构造类字符串类型:

type Color = 'red' | 'blue';
type Quantity = 'one' | 'two';

type SeussFish = `${Quantity | Color} fish`;
// 等同于
//   type SeussFish = "one fish" | "two fish"
//                  | "red fish" | "blue fish";

或者与其它类字符串类型进行模式匹配。

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;

// 正确
s1 = s2;

我们做的首个改动是 TypeScript 应该在何时去推断模版字符串类型。 当一个模版字符串的类型是由类字符串字面量类型进行的按上下文归类(比如,TypeScript 识别出将模版字符串传递给字面量类型时),它会得到模版字符串类型。

function bar(s: string): `hello ${string}` {
    // 之前会产生错误,但现在没有问题
    return `hello ${s}`;
}

在类型推断和 extends string 的类型参数上也会起作用。

declare let s: string;
declare function f<T extends string>(x: T): T;

// 以前:string
// 现在:`hello-${string}`
let x2 = f(`hello ${s}`);

另一个主要的改动是 TypeScript 会更好地进行类型关联,并在不同的模版字符串之间进行推断。

示例如下:

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;

s1 = s2;
s1 = s3;

在检查字符串字面量类型时,例如 s2,TypeScript 可以匹配字符串的内容并计算出在第一个赋值语句中 s2s1 兼容。 然而,当再次遇到模版字符串类型时,则会直接放弃进行匹配。 结果就是,像 s3s1 的赋值语句会出错。

现在,TypeScript 会去判断是否模版字符串的每一部分都能够成功匹配。 你现在可以混合并使用不同的替换字符串来匹配模版字符串,TypeScript 能够更好地计算出它们是否兼容。

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
declare let s4: `1-${number}-3`;
declare let s5: `1-2-${number}`;
declare let s6: `${number}-2-${number}`;

// 下列均无问题
s1 = s2;
s1 = s3;
s1 = s4;
s1 = s5;
s1 = s6;

在这项改进之后,TypeScript 提供了更好的推断能力。 示例如下:

declare function foo<V extends string>(arg: `*${V}*`): V;

function test<T extends string>(s: string, n: number, b: boolean, t: T) {
    let x1 = foo('*hello*'); // "hello"
    let x2 = foo('**hello**'); // "*hello*"
    let x3 = foo(`*${s}*` as const); // string
    let x4 = foo(`*${n}*` as const); // `${number}`
    let x5 = foo(`*${b}*` as const); // "true" | "false"
    let x6 = foo(`*${t}*` as const); // `${T}`
    let x7 = foo(`**${s}**` as const); // `*${string}*`
}

更多详情,请参考PR:利用按上下文归类,以及PR:改进模版字符串类型的类型推断和检查

ECMAScript #private 的类成员

TypeScript 4.3 扩大了在类中可被声明为 #private #names 的成员的范围,使得它们在运行时成为真正的私有的。 除属性外,方法和存取器也可进行私有命名。

class Foo {
    #someMethod() {
        //...
    }

    get #someValue() {
        return 100;
    }

    publicMethod() {
        // 可以使用
        // 可以在类内部访问私有命名成员。
        this.#someMethod();
        return this.#someValue;
    }
}

new Foo().#someMethod();
//        ~~~~~~~~~~~
// 错误!
// 属性 '#someMethod' 无法在类 'Foo' 外访问,因为它是私有的。

new Foo().#someValue;
//        ~~~~~~~~~~
// 错误!
// 属性 '#someValue' 无法在类 'Foo' 外访问,因为它是私有的。

更为广泛地,静态成员也可以有私有命名。

class Foo {
    static #someMethod() {
        // ...
    }
}

Foo.#someMethod();
//  ~~~~~~~~~~~
// 错误!
// 属性 '#someMethod' 无法在类 'Foo' 外访问,因为它是私有的。

该功能是由 Bloomberg 的朋友开发的:PR - 由 Titian Cernicova-DragomirKubilay Kahveci 开发,并得到了 Joey WattsRob PalmerTim McClure 的帮助支持。 感谢他们!

ConstructorParameters 可用于抽象类

在 TypeScript 4.3 中,ConstructorParameters工具类型可以用在 abstract 类上。

abstract class C {
    constructor(a: string, b: number) {
        // ...
    }
}

// 类型为 '[a: string, b: number]'
type CParams = ConstructorParameters<typeof C>;

这多亏了 TypeScript 4.2 支持了声明抽象的构造签名:

type MyConstructorOf<T> = {
    new (...args: any[]): T;
};

// 或使用简写形式:

type MyConstructorOf<T> = abstract new (...args: any[]) => T;

更多详情,请参考 PR

按上下文细化泛型类型

TypeScript 4.3 能够更智能地对泛型进行类型细化。 这让 TypeScript 能够支持更多模式,甚至有时还能够发现错误。

设想有这样的场景,我们想要编写一个 makeUnique 函数。 它接受一个 SetArray,如果接收的是 Array,则对数组进行排序并去除重复的元素。 最后返回初始的集合。

function makeUnique<T>(
  collection: Set<T> | T[],
  comparer: (x: T, y: T) => number
): Set<T> | T[] {
  // 假设元素已经是唯一的
  if (collection instanceof Set) {
    return collection;
  }

  // 排序,然后去重
  collection.sort(comparer);
  for (let i = 0; i < collection.length; i++) {
    let j = i;
    while (
      j < collection.length &&
      comparer(collection[i], collection[j + 1]) === 0
    ) {
      j++;
    }
    collection.splice(i + 1, j - i);
  }
  return collection;
}

暂且不谈该函数的具体实现,假设它就是某应用中的一个需求。 我们可能会注意到,函数签名没能捕获到 collection 的初始类型。 我们可以定义一个类型参数 C,并用它代替 Set<T> | T[]

- function makeUnique<T>(collection: Set<T> | T[], comparer: (x: T, y: T) => number): Set<T> | T[]
+ function makeUnique<T, C extends Set<T> | T[]>(collection: C, comparer: (x: T, y: T) => number): C

在 TypeScript 4.2 以及之前的版本中,如果这样做的话会产生很多错误。

function makeUnique<T, C extends Set<T> | T[]>(
  collection: C,
  comparer: (x: T, y: T) => number
): C {
  // 假设元素已经是唯一的
  if (collection instanceof Set) {
    return collection;
  }

  // 排序,然后去重
  collection.sort(comparer);
  //         ~~~~
  // 错误:属性 'sort' 不存在于类型 'C' 上。
  for (let i = 0; i < collection.length; i++) {
    //                           ~~~~~~
    // 错误: 属性 'length' 不存在于类型 'C' 上。
    let j = i;
    while (
      j < collection.length &&
      comparer(collection[i], collection[j + 1]) === 0
    ) {
      //             ~~~~~~
      // 错误: 属性 'length' 不存在于类型 'C' 上。
      //       ~~~~~~~~~~~~~  ~~~~~~~~~~~~~~~~~
      // 错误: 元素具有隐式的 'any' 类型,因为 'number' 类型的表达式不能用来索引 'Set<T> | T[]' 类型。
      j++;
    }
    collection.splice(i + 1, j - i);
    //         ~~~~~~
    // 错误: 属性 'splice' 不存在于类型 'C' 上。
  }
  return collection;
}

全是错误! 为何 TypeScript 要对我们如此刻薄?

问题在于进行 collection instanceof Set 检查时,我们期望它能够成为类型守卫,并根据条件将 Set<T> | T[] 类型细化为 Set<T>T[] 类型; 然而,实际上 TypeScript 没有对 Set<T> | T[] 进行处理,而是去细化泛型值 collection,其类型为 C

虽是细微的差别,但结果却不同。 TypeScript 不会去读取 C 的泛型约束(即 Set<T> | T[])并细化它。 如果要让 TypeScript 由 Set<T> | T[] 进行类型细化,它就会忘记在每个分支中 collection 的类型为 C,因为没有比较好的办法去保留这些信息。 假设 TypeScript 真这样做了,那么上例也会有其它的错误。 在函数返回的位置期望得到一个 C 类型的值,但从每个分支中得到的却是Set<T>T[],因此 TypeScript 会拒绝编译。

function makeUnique<T>(
  collection: Set<T> | T[],
  comparer: (x: T, y: T) => number
): Set<T> | T[] {
  // 假设元素已经是唯一的
  if (collection instanceof Set) {
    return collection;
    //     ~~~~~~~~~~
    // 错误:类型 'Set<T>' 不能赋值给类型 'C'。
    //          'Set<T>' 可以赋值给 'C' 的类型约束,但是
    //          'C' 可能使用 'Set<T> | T[]' 的不同子类型进行实例化。
  }

  // ...

  return collection;
  //     ~~~~~~~~~~
  // 错误:类型 'T[]' 不能赋值给类型 'C'。
  //          'T[]' 可以赋值给 'C' 的类型约束,但是
  //          'C' 可能使用 'Set<T> | T[]' 的不同子类型进行实例化。
}

TypeScript 4.3 是怎么做的? 在一些关键的位置,类型系统会去查看类型的约束。 例如,在遇到 collection.length 时,TypeScript 不去关心 collection 的类型为 C,而是会去查看可访问的属性,而这些是由 T[] | Set<T> 泛型约束决定的。

在类似的地方,TypeScript 会获取由泛型约束细化出的类型,因为它包含了用户关心的信息; 而在其它的一些地方,TypeScript 会去细化初始的泛型类型(但结果通常也是该泛型类型)。

换句话说,根据泛型值的使用方式,TypeScript 的处理方式会稍有不同。 最终结果就是,上例中的代码不会产生编译错误。

更多详情,请参考PR

检查总是为真的 Promise

strictNullChecks 模式下,在条件语句中检查 Promise 是否真时会产生错误。

async function foo(): Promise<boolean> {
  return false;
}

async function bar(): Promise<string> {
  if (foo()) {
    //  ~~~~~
    // Error!
    // This condition will always return true since
    // this 'Promise<boolean>' appears to always be defined.
    // Did you forget to use 'await'?
    return 'true';
  }
  return 'false';
}

这项改动是由Jack Works实现。

static 索引签名

与明确的类型声明相比,索引签名允许我们在一个值上设置更多的属性。

class Foo {
  hello = 'hello';
  world = 1234;

  // 索引签名:
  [propName: string]: string | number | undefined;
}

let instance = new Foo();

// 没问题
instance['whatever'] = 42;

// 类型为 'string | number | undefined'
let x = instance['something'];

目前为止,索引签名只允许在类的实例类型上进行设置。 感谢 Wenlu WangPR,现在索引签名也可以声明为 static

class Foo {
  static hello = 'hello';
  static world = 1234;

  static [propName: string]: string | number | undefined;
}

// 没问题
Foo['whatever'] = 42;

// 类型为 'string | number | undefined'
let x = Foo['something'];

类静态类型上的索引签名检查规则与类实例类型上的索引签名的检查规则是相同的,即每个静态属性必须与静态索引签名类型兼容。

class Foo {
  static prop = true;
  //     ~~~~
  // 错误!'boolean' 类型的属性 'prop' 不能赋值给字符串索引类型
  // 'string | number | undefined'.

  static [propName: string]: string | number | undefined;
}

.tsbuildinfo 文件大小改善

TypeScript 4.3 中,作为 --incremental 构建组分部分的 .tsbuildinfo 文件会变得非常小。 这得益于一些内部格式的优化,使用以数值标识的查找表来替代重复多次的完整路径以及类似的信息。 这项工作的灵感源自于 Tobias KoppersPR,而后在 PR 中实现,并在 PR 中进行优化。

我们观察到了 .tsbuildinfo 文件有如下的变化:

  • 1MB 到 411 KB
  • 14.9MB 到 1MB
  • 1345MB 到 467MB

不用说,缩小文件的尺寸会稍微加快构建速度。

--incremental--watch 中进行惰性计算

--incremental--watch 模式的一个问题是虽然它会加快后续的编译速度,但是首次编译很慢 - 有时会非常地慢。 这是因为在该模式下需要保存和计算当前工程的一些信息,有时还需要将这些信息写入 .tsbuildinfo 文件,以备后续之用。

因此, TypeScript 4.3 也对 --incremental--watch 进行了首次构建时的优化,让它可以和普通构建一样快。 为了达到目的,大部分信息会进行按需计算,而不是和往常一样全部一次性计算。 虽然这会加重后续构建的负担,但是 TypeScript 的 --incremental--watch 功能会智能地处理一小部分文件,并保存住会对后续构建有用的信息。 这就好比,--incremental--watch 构建会进行“预热”,并能够在多次修改文件后加速构建。

在一个包含了 3000 个文件的仓库中, 这能节约大概三分之一的构建时间

这项改进 是由 Tobias Koppers 开启,并在 PR 里完成。 感谢他们!

导入语句的补全

在 JavaScript 中,关于导入导出语句的一大痛点是其排序问题 - 尤其是导入语句的写法如下:

import { func } from './module.js';

而非

from "./module.js" import { func };

这导致了在书写完整的导入语句时很难受,因为自动补全无法工作。 例如,你输入了 import { ,TypeScript 不知道你要从哪个模块里导入,因此它不能提供补全信息。

为缓解该问题,我们可以利用自动导入功能! 自动导入能够提供每个可能导出并在文件顶端插入一条导入语句。

因此当你输入 import 语句并没提供一个路径时,TypeScript 会提供一个可能的导入列表。 当你确认了一个补全,TypeScript 会补全完整的导入语句,它包含了你要输入的路径。

Import statement completions

该功能需要编辑器的支持。 你可以在 Insiders 版本的 Visual Studio Code 中进行尝试。

更多详情,请参考 PR

TypeScript 现在能够理解 @link 标签,并会解析它指向的声明。 也就是说,你将鼠标悬停在 @link 标签上会得到一个快速提示,或者使用“跳转到定义”或“查找全部引用”命令。

例如,在支持 TypeScript 的编辑器中你可以在 @link bar中的 bar 上使用跳转到定义,它会跳转到 bar 的函数声明。

/**
 * To be called 70 to 80 days after {@link plantCarrot}.
 */
function harvestCarrot(carrot: Carrot) {}

/**
 * Call early in spring for best results. Added in v2.1.0.
 * @param seed Make sure it's a carrot seed!
 */
function plantCarrot(seed: Seed) {
  // TODO: some gardening
}

Jumping to definition and requesting quick info on a @link tag for

更多详情,请参考 PR

在非 JavaScript 文件上的跳转到定义

许多加载器允许用户在 JavaScript 的导入语句中导入资源文件。 例如典型的 import "./styles.css" 语句。

目前为止,TypeScript 的编辑器功能不会去尝试读取这些文件,因此“跳转到定义”会失败。 在最好的情况下,“跳转到定义”会跳转到类似 declare module "*.css" 这样的声明语句上,如果它能够找到的话。

现在,在执行“跳转到定义”命令时,TypeScript 的语言服务会尝试跳转到正确的文件,即使它们不是 JavaScript 或 TypeScript 文件! 在 CSS,SVGs,PNGs,字体文件,Vue 文件等的导入语句上尝试一下吧。

更多详情,请参考 PR

TypeScript 4.2

更智能地保留类型别名

在 TypeScript 中,使用类型别名能够给某个类型起个新名字。 倘若你定义了一些函数,并且它们全都使用了 string | number | boolean 类型,那么你就可以定义一个类型别名来避免重复。

type BasicPrimitive = number | string | boolean;

TypeScript 使用了一系列规则来推测是否该在显示类型时使用类型别名。 例如,有如下的代码。

export type BasicPrimitive = number | string | boolean;

export function doStuff(value: BasicPrimitive) {
    let x = value;
    return x;
}

如果在 Visual Studio,Visual Studio Code 或者 TypeScript 演练场编辑器中把鼠标光标放在 x 上,我们就会看到信息面板中显示出了 BasicPrimitive 类型。 同样地,如果我们查看由该文件生成的声明文件(.d.ts),那么 TypeScript 会显示出 doStuff 的返回值类型为 BasicPrimitive 类型。

那么你猜一猜,如果返回值类型为 BasicPrimitiveundefined 时会发生什么?

export type BasicPrimitive = number | string | boolean;

export function doStuff(value: BasicPrimitive) {
    if (Math.random() < 0.5) {
        return undefined;
    }

    return value;
}

可以在TypeScript 4.1 演练场中查看结果。 虽然我们希望 TypeScript 将 doStuff 的返回值类型显示为 BasicPrimitive | undefined,但是它却显示成了 string | number | boolean | undefined 类型! 这是怎么回事?

这与 TypeScript 内部的类型表示方式有关。 当基于一个联合类型来创建另一个联合类型时,TypeScript 会将类型标准化,也就是把类型展开为一个新的联合类型 - 但这么做也可能会丢失信息。 类型检查器不得不根据 string | number | boolean | undefined 类型来尝试每一种可能的组合并查看使用了哪些类型别名,即便这样也可能会有多个类型别名指向 string | number | boolean 类型。

TypeScript 4.2 的内部实现更加智能了。 我们会记录类型是如何被构造的,会记录它们原本的编写方式和之后的构造方式。 我们同样会记录和区分不同的类型别名!

有能力根据类型使用的方式来回显这个类型就意味着,对于 TypeScript 用户来讲能够避免显示很长的类型;同时也意味着会生成更友好的 .d.ts 声明文件、错误消息和编辑器内显示的类型及签名帮助信息。 这会让 TypeScript 对于初学者来讲更友好一些。

更多详情,请参考PR:改进保留类型别名的联合,以及PR:保留间接的类型别名

元组类型中前导的/中间的剩余元素

在 TypeScript 中,元组类型用于表示固定长度和元素类型的数组。

// 存储了一对数字的元组
let a: [number, number] = [1, 2];

// 存储了一个string,一个number和一个boolean的元组
let b: [string, number, boolean] = ['hello', 42, true];

随着时间的推移,TypeScript 中的元组类型变得越来越复杂,因为它们也被用来表示像 JavaScript 中的参数列表类型。 结果就是,它可能包含可选元素和剩余元素,以及用于工具和提高可读性的标签。

// 包含一个或两个元素的元组。
let c: [string, string?] = ['hello'];
c = ['hello', 'world'];

// 包含一个或两个元素的标签元组。
let d: [first: string, second?: string] = ['hello'];
d = ['hello', 'world'];

// 包含剩余元素的元组 - 至少前两个元素是字符串,
// 以及后面的任意数量的布尔元素。
let e: [string, string, ...boolean[]];

e = ['hello', 'world'];
e = ['hello', 'world', false];
e = ['hello', 'world', true, false, true];

在 TypeScript 4.2 中,剩余元素会按它们的使用方式进行展开。 在之前的版本中,TypeScript 只允许 ...rest 元素位于元组的末尾。

但现在,剩余元素可以出现在元组中的任意位置 - 但有一点限制。

let foo: [...string[], number];

foo = [123];
foo = ['hello', 123];
foo = ['hello!', 'hello!', 'hello!', 123];

let bar: [boolean, ...string[], boolean];

bar = [true, false];
bar = [true, 'some text', false];
bar = [true, 'some', 'separated', 'text', false];

唯一的限制是,剩余元素之后不能出现可选元素或其它剩余元素。 换句话说,一个元组中只允许有一个剩余元素,并且剩余元素之后不能有可选元素。

interface Clown {
    /*...*/
}
interface Joker {
    /*...*/
}

let StealersWheel: [...Clown[], 'me', ...Joker[]];
//                                    ~~~~~~~~~~ 错误

let StringsAndMaybeBoolean: [...string[], boolean?];
//                                        ~~~~~~~~ 错误

这些不在结尾的剩余元素能够用来描述,可接收任意数量的前导参数加上固定数量的结尾参数的函数。

declare function doStuff(
    ...args: [...names: string[], shouldCapitalize: boolean]
): void;

doStuff(/*shouldCapitalize:*/ false);
doStuff('fee', 'fi', 'fo', 'fum', /*shouldCapitalize:*/ true);

尽管 JavaScript 中没有声明前导剩余参数的语法,但我们仍可以将 doStuff 函数的参数声明为带有前导剩余元素 ...args 的元组类型。 使用这种方式可以帮助我们描述许多的 JavaScript 代码!

更多详情,请参考 PR

更严格的 in 运算符检查

在 JavaScript 中,如果 in 运算符的右操作数是非对象类型,那么会产生运行时错误。 TypeScript 4.2 确保了该错误能够在编译时被捕获。

'foo' in 42;
// The right-hand side of an 'in' expression must not be a primitive.

这个检查在大多数情况下是相当保守的,如果你看到提示了这个错误,那么代码中很可能真的有问题。

非常感谢外部贡献者 Jonas HübotterPR

--noPropertyAccessFromIndexSignature

在 TypeScript 刚开始支持索引签名时,它只允许使用方括号语法来访问索引签名中定义的元素,例如 person["name"]

interface SomeType {
    /** 这是索引签名 */
    [propName: string]: any;
}

function doStuff(value: SomeType) {
    let x = value['someProperty'];
}

这就导致了在处理带有任意属性的对象时变得烦锁。 例如,假设有一个容易出现拼写错误的 API,容易出现在属性名的末尾位置多写一个字母 s 的错误。

interface Options {
    /** 要排除的文件模式。 */
    exclude?: string[];

    /**
     * 这会将其余所有未声明的属性定义为 'any' 类型。
     */
    [x: string]: any;
}

function processOptions(opts: Options) {
    // 注意,我们想要访问 `excludes` 而不是 `exclude`
    if (opts.excludes) {
        console.error(
            'The option `excludes` is not valid. Did you mean `exclude`?'
        );
    }
}

为了便于处理以上情况,在从前的时候,TypeScript 允许使用点语法来访问通过字符串索引签名定义的属性。 这会让从 JavaScript 代码到 TypeScript 代码的迁移工作变得容易。

然而,放宽限制同样意味着更容易出现属性名拼写错误。

interface Options {
    /** 要排除的文件模式。 */
    exclude?: string[];

    /**
     * 这会将其余所有未声明的属性定义为 'any' 类型。
     */
    [x: string]: any;
}
// ---cut---
function processOptions(opts: Options) {
    // ...

    // 注意,我们不小心访问了错误的 `excludes`。
    // 但是!这是合法的!
    for (const excludePattern of opts.excludes) {
        // ...
    }
}

在某些情况下,用户会想要选择使用索引签名 - 在使用点号语法进行属性访问时,如果访问了没有明确定义的属性,就得到一个错误。

这就是为什么 TypeScript 引入了一个新的 --noPropertyAccessFromIndexSignature 编译选项。 在该模式下,你可以有选择的启用 TypeScript 之前的行为,即在上述使用场景中产生错误。 该编译选项不属于 strict 编译选项集合的一员,因为我们知道该功能只适用于部分用户。

更多详情,请参考 PR。 我们同时要感谢 Wenlu Wang 为该功能的付出!

abstract 构造签名

TypeScript 允许将一个类标记为 abstract。 这相当于告诉 TypeScript 这个类只是用于继承,并且有些成员需要在子类中实现,以便能够真正地创建出实例。

abstract class Shape {
    abstract getArea(): number;
}

// 不能创建抽象类的实例
new Shape();

class Square extends Shape {
    #sideLength: number;

    constructor(sideLength: number) {
        super();
        this.#sideLength = sideLength;
    }

    getArea() {
        return this.#sideLength ** 2;
    }
}

// 没问题
new Square(42);

为了能够确保一贯的对 new 一个 abstract 类进行限制,不允许将 abstract 类赋值给接收构造签名的值。

abstract class Shape {
    abstract getArea(): number;
}

interface HasArea {
    getArea(): number;
}

// 不能将抽象构造函数类型赋值给非抽象构造函数类型。
let Ctor: new () => HasArea = Shape;

如果有代码调用了 new Ctor,那么上述的行为是正确的,但若想要编写 Ctor 的子类,就会出现过度限制的情况。

abstract class Shape {
    abstract getArea(): number;
}

interface HasArea {
    getArea(): number;
}

function makeSubclassWithArea(Ctor: new () => HasArea) {
    return class extends Ctor {
        getArea() {
            return 42;
        }
    };
}

// 不能将抽象构造函数类型赋值给非抽象构造函数类型。
let MyShape = makeSubclassWithArea(Shape);

对于内置的工具类型InstanceType来讲,它也不是工作得很好。

// 错误!
// 不能将抽象构造函数类型赋值给非抽象构造函数类型。
type MyInstance = InstanceType<typeof Shape>;

这就是为什么 TypeScript 4.2 允许在构造签名上指定 abstract 修饰符。

abstract class Shape {
  abstract getArea(): number;
}
// ---cut---
interface HasArea {
    getArea(): number;
}

// Works!
let Ctor: abstract new () => HasArea = Shape;

在构造签名上添加 abstract 修饰符表示可以传入一个 abstract 构造函数。 它不会阻止你传入其它具体的类/构造函数 - 它只是想表达不会直接调用这个构造函数,因此可以安全地传入任意一种类类型。

这个特性允许我们编写支持抽象类的混入工厂函数。 例如,在下例中,我们可以同时使用混入函数 withStylesabstractSuperClass

abstract class SuperClass {
    abstract someMethod(): void;
    badda() {}
}

type AbstractConstructor<T> = abstract new (...args: any[]) => T

function withStyles<T extends AbstractConstructor<object>>(Ctor: T) {
    abstract class StyledClass extends Ctor {
        getStyles() {
            // ...
        }
    }
    return StyledClass;
}

class SubClass extends withStyles(SuperClass) {
    someMethod() {
        this.someMethod()
    }
}

注意,withStyles 展示了一个特殊的规则,若一个类(StyledClass)继承了被抽象构造函数所约束的泛型值,那么这个类也需要被声明为 abstract。 由于无法知道传入的类是否拥有更多的抽象成员,因此也无法知道子类是否实现了所有的抽象成员。

更多详情,请参考 PR

使用 --explainFiles 来理解工程的结构

TypeScript 用户时常会问“为什么 TypeScript 包含了这个文件?”。 推断程序中所包含的文件是个很复杂的过程,比如有很多原因会导致使用了 lib.d.ts 文件的组合,会导致 node_modules 中的文件被包含进来,会导致有些已经 exclude 的文件被包含进来。

这就是 TypeScript 提供 --explainFiles 的原因。

tsc --explainFiles

在使用了该选项时,TypeScript 编译器会输出非常详细的信息来说明某个文件被包含进工程的原因。 为了更易理解,我们可以把输出结果存到文件里,或者通过管道使用其它命令来查看它。

# 将输出保存到文件
tsc --explainFiles > expanation.txt

# 将输出传递给工具程序 `less`,或编辑器 VS Code
tsc --explainFiles | less

tsc --explainFiles | code -

通常,输出结果首先会给列出包含 lib.d.ts 文件的原因,然后是本地文件,再然后是 node_modules 文件。

TS_Compiler_Directory/4.2.2/lib/lib.es5.d.ts
  Library referenced via 'es5' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts
  Library referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts
  Library referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts
  Library referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts
  Library referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts
  Library referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts
  Library referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts
  Library 'lib.esnext.d.ts' specified in compilerOptions

... More Library References...

foo.ts
  Matched by include pattern '**/*' in 'tsconfig.json'

目前,TypeScript 不保证输出文件的格式 - 它在将来可能会改变。 关于这一点,我们也打算改进输出文件格式,请给出你的建议!

更多详情,请参考 PR

改进逻辑表达式中的未被调用函数检查

感谢 Alex Tarasyuk 提供的持续改进,TypeScript 中的未调用函数检查现在也作用于 &&|| 表达式。

--strictNullChecks 模式下,下面的代码会产生错误。

function shouldDisplayElement(element: Element) {
    // ...
    return true;
}

function getVisibleItems(elements: Element[]) {
    return elements.filter((e) => shouldDisplayElement && e.children.length);
    //                          ~~~~~~~~~~~~~~~~~~~~
    // 该条件表达式永远返回 true,因为函数永远是定义了的。
    // 你是否想要调用它?
}

更多详情,请参考 PR

解构出来的变量可以被明确地标记为未使用的

感谢 Alex Tarasyuk 提供的另一个 PR,你可以使用下划线(_ 字符)将解构变量标记为未使用的。

let [_first, second] = getValues();

在之前,如果 _first 未被使用,那么在启用了 noUnusedLocals 时 TypeScript 会产生一个错误。 现在,TypeScript 会识别出使用了下划线的 _first 变量是有意的未使用的变量。

更多详情,请参考 PR

放宽了在可选属性和字符串索引签名间的限制

字符串索引签名可用于为类似于字典的对象添加类型,它表示允许使用任意的键来访问对象:

const movieWatchCount: { [key: string]: number } = {};

function watchMovie(title: string) {
    movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}

当然了,对于不在字典中的电影名而言 movieWatchCount[title] 的值为 undefined。(TypeScript 4.1 增加了 --noUncheckedIndexedAccess 选项,在访问索引签名时会增加 undefined 值。) 即便一定会有 movieWatchCount 中不存在的属性,但在之前的版本中,由于 undefined 值的存在,TypeScript 会将可选对象属性视为不可以赋值给兼容的索引签名。

type WesAndersonWatchCount = {
    'Fantastic Mr. Fox'?: number;
    'The Royal Tenenbaums'?: number;
    'Moonrise Kingdom'?: number;
    'The Grand Budapest Hotel'?: number;
};

declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
//    ~~~~~~~~~~~~~~~ 错误!
// 类型 'WesAndersonWatchCount' 不允许赋值给类型 '{ [key: string]: number; }'。
//    属性 '"Fantastic Mr. Fox"' 与索引签名不兼容。
//      类型 'number | undefined' 不允许赋值给类型 'number'。
//        类型 'undefined' 不允许赋值给类型 'number'。 (2322)

TypeScript 4.2 允许这样赋值。 但是不允许使用带有 undefined 类型的非可选属性进行赋值,也不允许将 undefined 值直接赋值给某个属性:

type BatmanWatchCount = {
    'Batman Begins': number | undefined;
    'The Dark Knight': number | undefined;
    'The Dark Knight Rises': number | undefined;
};

declare const batmanWatchCount: BatmanWatchCount;

// 在 TypeScript 4.2 中仍是错误。
const movieWatchCount: { [key: string]: number } = batmanWatchCount;

// 在 TypeScript 4.2 中仍是错误。
// 索引签名不允许显式地赋值 `undefined`。
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;

这条新规则不适用于数字索引签名,因为它们被当成是类数组的并且是稠密的:

declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };

sortOfArrayish = numberKeys;

更多详情,请参考 PR

声明缺失的函数

感谢 Alexander Tarasyuk 提交的 PR,TypeScript 支持了一个新的快速修复功能,那就是根据调用方来生成新的函数和方法声明!

一个未被声明的 foo 函数被调用了,使用快速修复

TypeScript 4.1

模版字面量类型

使用字符串字面量类型能够表示仅接受特定字符串参数的函数和 API。

function setVerticalAlignment(location: 'top' | 'middle' | 'bottom') {
    // ...
}

setVerticalAlignment('middel');
//                   ^^^^^^^^
// Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.

使用字符串字面量类型的好处是它能够对字符串进行拼写检查。

此外,字符串字面量还能用于映射类型中的属性名。 从这个意义上来讲,它们可被当作构件使用。

type Options = {
    [K in
        | 'noImplicitAny'
        | 'strictNullChecks'
        | 'strictFunctionTypes']?: boolean;
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

还有一处字符串字面量类型可被当作构件使用,那就是在构造其它字符串字面量类型时。

这也是 TypeScript 4.1 支持模版字面量类型的原因。 它的语法与JavaScript 中的模版字面量的语法是一致的,但是是用在表示类型的位置上。 当将其与具体类型结合使用时,它会将字符串拼接并产生一个新的字符串字面量类型。

type World = 'world';

type Greeting = `hello ${World}`;
//   ^^^^^^^^^
//   "hello world"

如果在替换的位置上使用了联合类型会怎么样呢? 它将生成由各个联合类型成员所表示的字符串字面量类型的联合。

type Color = 'red' | 'blue';
type Quantity = 'one' | 'two';

type SeussFish = `${Quantity | Color} fish`;
//   ^^^^^^^^^
//   "one fish" | "two fish" | "red fish" | "blue fish"

除此之外,我们也可以在其它场景中应用它。 例如,有些 UI 组件库提供了指定垂直和水平对齐的 API,通常会使用类似于"bottom-right"的字符串来同时指定。 在垂直对齐的选项"top""middle""bottom",以及水平对齐的选项"left""center""right"之间,共有 9 种可能的字符串,前者选项之一与后者选项之一之间使用短横线连接。

type VerticalAlignment = 'top' | 'middle' | 'bottom';
type HorizontalAlignment = 'left' | 'center' | 'right';

// Takes
//   | "top-left"    | "top-center"    | "top-right"
//   | "middle-left" | "middle-center" | "middle-right"
//   | "bottom-left" | "bottom-center" | "bottom-right"

declare function setAlignment(
    value: `${VerticalAlignment}-${HorizontalAlignment}`
): void;

setAlignment('top-left'); // works!
setAlignment('top-middel'); // error!
setAlignment('top-pot'); // error! but good doughnuts if you're ever in Seattle

这样的例子还有很多,但它仍只是小例子而已,因为我们可以直接写出所有可能的值。 实际上,对于 9 个字符串来讲还算可以;但是如果需要大量的字符串,你就得考虑如何去自动生成(或者简单地使用string)。

有些值实际上是来自于动态创建的字符串字面量。 例如,假设 makeWatchedObject API 接收一个对象,并生成一个几乎等同的对象,但是带有一个新的on方法来检测属性的变化。

let person = makeWatchedObject({
    firstName: 'Homer',
    age: 42,
    location: 'Springfield',
});

person.on('firstNameChanged', () => {
    console.log(`firstName was changed!`);
});

注意,on监听的是"firstNameChanged"事件,而非仅仅是"firstName"。 那么我们如何定义类型?

type PropEventSource<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};

/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

这样做的话,如果传入了错误的属性会产生一个错误!

type PropEventSource<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
let person = makeWatchedObject({
    firstName: 'Homer',
    age: 42,
    location: 'Springfield',
});

// error!
person.on('firstName', () => {});

// error!
person.on('frstNameChanged', () => {});

我们还可以在模版字面量上做一些其它的事情:可以从替换的位置来推断类型。 我们将上面的例子改写成泛型,由eventName字符串来推断关联的属性名。

type PropEventSource<T> = {
    on<K extends string & keyof T>(
        eventName: `${K}Changed`,
        callback: (newValue: T[K]) => void
    ): void;
};

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

let person = makeWatchedObject({
    firstName: 'Homer',
    age: 42,
    location: 'Springfield',
});

// works! 'newName' is typed as 'string'
person.on('firstNameChanged', (newName) => {
    // 'newName' has the type of 'firstName'
    console.log(`new name is ${newName.toUpperCase()}`);
});

// works! 'newAge' is typed as 'number'
person.on('ageChanged', (newAge) => {
    if (newAge < 0) {
        console.log('warning! negative age');
    }
});

这里我们将on定义为泛型方法。 当用户使用"firstNameChanged'来调用该方法,TypeScript 会尝试去推断出K所表示的类型。 为此,它尝试将K"Changed"之前的内容进行匹配并推断出"firstName"。 一旦 TypeScript 得到了结果,on方法就能够从原对象上获取firstName的类型,此例中是string。 类似地,当使用"ageChanged"调用时,它会找到属性age的类型为number

类型推断可以用不同的方式组合,常见的是解构字符串,再使用其它方式重新构造它们。 实际上,为了便于修改字符串字面量类型,我们引入了一些新的工具类型来修改字符大小写。

type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`;

type HELLO = EnthusiasticGreeting<'hello'>;
//   ^^^^^
//   "HELLO"

新的类型别名为UppercaseLowercaseCapitalizeUncapitalize。 前两个会转换字符串中的所有字符,而后面两个只转换字符串的首字母。

更多详情,查看原 PR以及正在进行中的切换类型别名助手的 PR.

在映射类型中更改映射的键

让我们先回顾一下,映射类型可以使用任意的键来创建新的对象类型。

type Options = {
    [K in
        | 'noImplicitAny'
        | 'strictNullChecks'
        | 'strictFunctionTypes']?: boolean;
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

或者,基于任意的对象类型来创建新的对象类型。

/// 'Partial<T>' 等同于 'T',只是把每个属性标记为可选的。
type Partial<T> = {
    [K in keyof T]?: T[K];
};

到目前为止,映射类型只能使用提供给它的键来创建新的对象类型;然而,很多时候我们想要创建新的键,或者过滤掉某些键。

这就是 TypeScript 4.1 允许更改映射类型中的键的原因。它使用了新的as语句。

type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K];
    //            ^^^^^^^^^^^^^
    //            这里是新的语法!
};

通过as语句,你可以利用例如模版字面量类型,并基于原属性名来轻松地创建新属性名。

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

type LazyPerson = Getters<Person>;
// type LazyPerson = {
//     getName: () => string;
//     getAge: () => number;
//     getLocation: () => string;
// }

此外,你可以巧用never类型来过滤掉某些键。 也就是说,在某些情况下你不必使用Omit工具类型。

// 删除 'kind' 属性
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, 'kind'>]: T[K];
};

interface Circle {
    kind: 'circle';
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;

type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, 'kind'>]: T[K];
};

interface Circle {
    kind: 'circle';
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
// type KindlessCircle = {
//     radius: number;
// }

更多详情,请参考PR

递归的有条件类型

在 JavaScript 中较为常见的是,一个函数能够以任意的层级来展平(flatten)并构建容器类型。 例如,可以拿Promise实例对象上的.then()方法来举例。 .then(...)方法能够拆解每一个Promise,直到它找到一个非Promise的值,然后将该值传递给回调函数。 Array上也存在一个相对较新的flat方法,它接收一个表示深度的参数,并以此来决定展平操作的层数。

在过去,我们无法使用 TypeScript 类型系统来表达上述例子。 虽然也存在一些 hack,但基本上都不切合实际。

TypeScript 4.1 取消了对有条件类型的一些限制 - 因此它现在可以表达上述类型。 在 TypeScript 4.1 中,允许在有条件类型的分支中立即引用该有条件类型自身,这就使得编写递归的类型别名变得更加容易。 例如,我们想定义一个类型来获取嵌套数组中的元素类型,可以定义如下的deepFlatten类型。

type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;

function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
    throw 'not implemented';
}

// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

类似地,在 TypeScript 4.1 中我们可以定义Awaited类型来拆解Promise

type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

/// 类似于 `promise.then(...)`,但是类型更准确
declare function customThen<T, U>(
    p: Promise<T>,
    onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;

一定要注意,虽然这些递归类型很强大,但要有节制地使用它。

首先,这些类型能做的更多,但也会增加类型检查的耗时。 尝试为考拉兹猜想或斐波那契数列建模是一件有趣的事儿,但请不要在 npm 上发布带有它们的.d.ts文件。

除了计算量大之外,这些类型还可能会达到内置的递归深度限制。 如果到达了递归深度限制,则会产生编译错误。 通常来讲,最好不要去定义这样的类型。

更多详情,请参考PR.

索引访问类型检查(--noUncheckedIndexedAccess

TypeScript 支持一个叫做索引签名的功能。 索引签名用于告诉类型系统,用户可以访问任意名称的属性。

interface Options {
    path: string;
    permissions: number;

    // 额外的属性可以被这个签名捕获
    [propName: string]: string | number;
}

function checkOptions(opts: Options) {
    opts.path; // string
    opts.permissions; // number

    // 以下都是允许的
    // 它们的类型为 'string | number'
    opts.yadda.toString();
    opts['foo bar baz'].toString();
    opts[Math.random()].toString();
}

上例中,Options包含了索引签名,它表示在访问未直接列出的属性时得到的类型为string | number。 这是一种乐观的做法,它假想我们非常清楚代码在做什么,但实际上 JavaScript 中的大部分值并不支持任意的属性名。 例如,大多数类型并不包含属性名为Math.random()的值。 对许多用户来讲,这不是期望的行为,就好像没有利用到--strictNullChecks提供的严格类型检查。

这就是 TypeScript 4.1 提供了--noUncheckedIndexedAccess编译选项的原因。 在该新模式下,任何属性访问(例如foo.bar)或者索引访问(例如foo["bar"])都会被认为可能为undefined。 例如在上例中,opts.yadda的类型为string | number | undefined,而不是string | number。 如果需要访问那个属性,你可以先检查属性是否存在或者使用非空断言运算符(!后缀字符)。

// @noUncheckedIndexedAccess
interface Options {
    path: string;
    permissions: number;

    // 额外的属性可以被这个签名捕获
    [propName: string]: string | number;
}
// ---cut---
function checkOptions(opts: Options) {
    opts.path; // string
    opts.permissions; // number

    // 在 noUncheckedIndexedAccess 下,以下操作不允许
    opts.yadda.toString();
    opts['foo bar baz'].toString();
    opts[Math.random()].toString();

    // 首先检查是否存在
    if (opts.yadda) {
        console.log(opts.yadda.toString());
    }

    // 使用 ! 非空断言,“我知道在做什么”
    opts.yadda!.toString();
}

使用--noUncheckedIndexedAccess的一个结果是,通过索引访问数组元素时也会进行严格类型检查,就算是在遍历检查过边界的数组时。

// @noUncheckedIndexedAccess
function screamLines(strs: string[]) {
    // 下面会有问题
    for (let i = 0; i < strs.length; i++) {
        console.log(strs[i].toUpperCase());
    }
}

如果你不需要使用索引,那么可以使用for-of循环或forEach来遍历。

// @noUncheckedIndexedAccess
function screamLines(strs: string[]) {
    // 可以正常工作
    for (const str of strs) {
        console.log(str.toUpperCase());
    }

    // 可以正常工作
    strs.forEach((str) => {
        console.log(str.toUpperCase());
    });
}

这个选项虽可以用来捕获访问越界的错误,但对大多数代码来讲有些烦,因此它不会被--strict选项自动启用;然而,如果你对此选项感兴趣,可以尝试一下,看它是否适用于你的代码。

更多详情,请参考PR.

不带 baseUrlpaths

路径映射的使用很常见 - 通常它用于优化导入语句,以及模拟在单一代码仓库中进行链接的行为。

不幸的是,在使用paths时必须指定baseUrl,它允许裸路径描述符基于baseUrl进行解析。 它会导致在自动导入时会使用较差的路径。

在 TypeScript 4.1 中,paths不必与baseUrl一起使用。 它会一定程序上帮助解决上述的问题。

checkJs 默认启用 allowJs

从前,如果你想要对 JavaScript 工程执行类型检查,你需要同时启用allowJscheckJs。 这样的体验让人讨厌,因此现在checkJs会默认启用allowJs

更多详情,请参考PR

React 17 JSX 工厂

TypeScript 4.1 通过以下两个编译选项来支持 React 17 中的jsxjsxs工厂函数:

  • react-jsx
  • react-jsxdev

这两个编译选项分别用于生产环境和开发环境中。 通常,编译选项之间可以继承。 例如,用于生产环境的tsconfig.json如下:

// ./src/tsconfig.json
{
    "compilerOptions": {
        "module": "esnext",
        "target": "es2015",
        "jsx": "react-jsx",
        "strict": true
    },
    "include": ["./**/*"]
}

另外一个用于开发环境的tsconfig.json如下:

// ./src/tsconfig.dev.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "jsx": "react-jsxdev"
    }
}

更多详情,请参考PR

在编辑器中支持 JSDoc @see 标签

编辑器对 TypeScript 和 JavaScript 代码中的 JSDoc 标签@see有了更好的支持。 它允许你使用像“跳转到定义”这样的功能。 例如,在下例中的 JSDoc 里可以使用跳转到定义到firstC

// @filename: first.ts
export class C {}

// @filename: main.ts
import * as first from './first';

/**
 * @see first.C
 */
function related() {}

感谢贡献者Wenlu Wang实现了这个功能

破坏性改动

lib.d.ts 更新

lib.d.ts包含一些 API 变动,在某种程度上是因为 DOM 类型是自动生成的。 一个具体的变动是Reflect.enumerate被删除了,因为它在 ES2016 中被删除了。

abstract 成员不能被标记为 async

abstract成员不再可以被标记为async。 这可以通过删除async关键字来修复,因为调用者只关注返回值类型。

any/unknown Are Propagated in Falsy Positions

从前,对于表达式foo && somethingElse,若foo的类型为anyunknown,那么整个表达式的类型为somethingElse

例如,在以前此处的x的类型为{ someProp: string }

declare let foo: unknown;
declare let somethingElse: { someProp: string };

let x = foo && somethingElse;

然而,在 TypeScript 4.1 中,会更谨慎地确定该类型。 由于不清楚&&左侧的类型,我们会传递anyunknown类型,而不是&&右侧的类型。

常见的模式是检查与boolean的兼容性,尤其是在谓词函数中。

function isThing(x: any): boolean {
    return x && typeof x === 'object' && x.blah === 'foo';
}

一种合适的修改是使用!!foo && someExpression来代替foo && someExpression

Promiseresolve的参数不再是可选的

在编写如下的代码时

new Promise((resolve) => {
    doSomethingAsync(() => {
        doSomething();
        resolve();
    });
});

你可能会得到如下的错误:

  resolve()
  ~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
  An argument for 'value' was not provided.

这是因为resolve不再有可选参数,因此默认情况下,必须给它传值。 它通常能够捕获Promise的 bug。 典型的修复方法是传入正确的参数,以及添加明确的类型参数。

new Promise<number>((resolve) => {
    //     ^^^^^^^^
    doSomethingAsync((value) => {
        doSomething();
        resolve(value);
        //      ^^^^^
    });
});

然而,有时resolve()确实需要不带参数来调用 在这种情况下,我们可以给Promise传入明确的void泛型类型参数(例如,Promise<void>)。 它利用了 TypeScript 4.1 中的一个新功能,一个潜在的void类型的末尾参数会变成可选参数。

new Promise<void>((resolve) => {
    //     ^^^^^^
    doSomethingAsync(() => {
        doSomething();
        resolve();
    });
});

TypeScript 4.1 提供了快速修复选项来解决该问题。

有条件展开会创建可选属性

在 JavaScript 中,对象展开(例如,{ ...foo })不会操作假值。 因此,在{ ...foo }代码中,如果foo的值为nullundefined,则它会被略过。

很多人利用该性质来可选地展开属性。

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

interface Animal {
    name: string;
    owner: Person;
}

function copyOwner(pet?: Animal) {
    return {
        ...(pet && pet.owner),
        otherStuff: 123,
    };
}

// We could also use optional chaining here:

function copyOwner(pet?: Animal) {
    return {
        ...pet?.owner,
        otherStuff: 123,
    };
}

此处,如果pet定义了,那么pet.owner的属性会被展开 - 否则,不会有属性被展开到目标对象中。

在之前,copyOwner的返回值类型为基于每个展开运算结果的联合类型: The return type of copyOwner was previously a union type based on each spread:

{ x: number } | { x: number, name: string, age: number, location: string }

它精确地展示了操作是如何进行的:如果pet定义了,那么Person中的所有属性都存在;否则,在结果中不存在Person中的任何属性。 它是一种要么全有要么全无的的操作。

然而,我们发现这个模式被过度地使用了,在单一对象中存在数以百计的展开运算,每一个展开操作可能会添加成百上千的操作。 结果就是这项操作可能非常耗时,并且用处不大。

在 TypeScript 4.1 中,返回值类型有时会使用全部的可选类型。

{
    x: number;
    name?: string;
    age?: number;
    location?: string;
}

这样的结果是有更好的性能以及更佳地展示。

更多详情,请参考PR。 目前,该行为还不完全一致,我们期待在未来会有所改进。

从前 TypeScript 在关联参数时,如果参数之间没有联系,则会将其关联为any类型。 由于TypeScript 4.1 的改动,TypeScript 会完全跳过这个过程。 这意味着一些可赋值性检查会失败,同时也意味着重载解析可能会失败。 例如,在解析 Node.js 中util.promisify函数的重载时可能会选择不同的重载签名,这可能会导致产生新的错误。

做为一个变通方法,你可能需要使用类型断言来消除错误。

TypeScript 4.0

可变参元组类型

在JavaScript中有一个函数concat,它接受两个数组或元组并将它们连接在一起构成一个新数组。

function concat(arr1, arr2) {
  return [...arr1, ...arr2];
}

再假设有一个tail函数,它接受一个数组或元组并返回除首个元素外的所有元素。

function tail(arg) {
  const [_, ...result] = arg;
  return result;
}

那么,我们如何在TypeScript中为这两个函数添加类型?

在旧版本的TypeScript中,对于concat函数我们能做的是编写一些函数重载签名。

function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

在保持第二个数组为空的情况下,我们已经编写了七个重载签名。 接下来,让我们为arr2添加一个参数。

function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

这已经开始变得不合理了。 不巧的是,在给tail函数添加类型时也会遇到同样的问题。

在受尽了“重载的折磨”后,它依然没有完全解决我们的问题。 它只能针对已编写的重载给出正确的类型。 如果我们想要处理所有情况,则还需要提供一个如下的重载:

function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;

但是这个重载签名没有反映出输入的长度,以及元组元素的顺序。

TypeScript 4.0带来了两项基础改动,还伴随着类型推断的改善,因此我们能够方便地添加类型。

第一个改动是展开元组类型的语法支持泛型。 这就是说,我们能够表示在元组和数组上的高阶操作,尽管我们不清楚它们的具体类型。 在实例化泛型展开时 当在这类元组上进行泛型展开实例化(或者使用实际类型参数进行替换)时,它们能够产生另一组数组和元组类型。

例如,我们可以像下面这样给tail函数添加类型,避免了“重载的折磨”。

function tail<T extends any[]>(arr: readonly [any, ...T]) {
  const [_ignored, ...rest] = arr;
  return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

const r1 = tail(myTuple);
//    [2, 3, 4]

const r2 = tail([...myTuple, ...myArray] as const);
//    [2, 3, 4, ...string[]]

第二个改动是,剩余元素可以出现在元组中的任意位置上 - 不只是末尾位置!

type Strings = [string, string];
type Numbers = [number, number];

type StrStrNumNumBool = [...Strings, ...Numbers, boolean];
//   [string, string, number, number, boolean]

在以前,TypeScript会像下面这样产生一个错误:

剩余元素必须出现在元组类型的末尾。

但是在TypeScript 4.0中放开了这个限制。

注意,如果展开一个长度未知的类型,那么后面的所有元素都将被纳入到剩余元素类型。

type Strings = [string, string];
type Numbers = number[];

type Unbounded = [...Strings, ...Numbers, boolean];
//   [string, string, ...(number | boolean)[]]

结合使用这两种行为,我们能够为concat函数编写一个良好的类型签名:

type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
  return [...arr1, ...arr2];
}

虽然这个签名仍有点长,但是我们不再需要像重载那样重复多次,并且对于任何数组或元组它都能够给出期望的类型。

该功能本身已经足够好了,但是它的强大更体现在一些复杂的场景中。 例如,考虑有一个支持部分参数应用的函数partialCallpartialCall接受一个函数(例如叫作f),以及函数f需要的一些初始参数。 它返回一个新的函数,该函数接受f需要的额外参数,并最终以初始参数和额外参数来调用f

function partialCall(f, ...headArgs) {
  return (...tailArgs) => f(...headArgs, ...tailArgs);
}

TypeScript 4.0改进了剩余参数和剩余元组元素的类型推断,因此我们可以为这种使用场景添加类型。

type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
  f: (...args: [...T, ...U]) => R,
  ...headArgs: T
) {
  return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}

此例中,partialCall知道能够接受哪些初始参数,并返回一个函数,它能够正确地选择接受或拒绝额外的参数。

// @errors: 2345 2554 2554 2345
type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
  f: (...args: [...T, ...U]) => R,
  ...headArgs: T
) {
  return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}
// ---cut---
const foo = (x: string, y: number, z: boolean) => {};

const f1 = partialCall(foo, 100);
//                          ~~~
// Argument of type 'number' is not assignable to parameter of type 'string'.

const f2 = partialCall(foo, "hello", 100, true, "oops");
//                                              ~~~~~~
// Expected 4 arguments, but got 5.(2554)

// This works!
const f3 = partialCall(foo, "hello");
//    (y: number, z: boolean) => void

// What can we do with f3 now?

// Works!
f3(123, true);

f3();

f3(123, "hello");

可变参元组类型支持了许多新的激动人心的模式,尤其是函数组合。 我们期望能够通过它来为JavaScript内置的bind函数进行更好的类型检查。 还有一些其它的类型推断改进以及模式引入进来,如果你想了解更多,请参考PR

标签元组元素

改进元组类型和参数列表使用体验的重要性在于它允许我们为JavaScript中惯用的方法添加强类型验证 - 例如对参数列表进行切片而后传递给其它函数。 这里至关重要的一点是我们可以使用元组类型作为剩余参数类型。

例如,下面的函数使用元组类型作为剩余参数:

function foo(...args: [string, number]): void {
  // ...
}

它与下面的函数基本没有区别:

function foo(arg0: string, arg1: number): void {
  // ...
}

对于foo函数的任意调用者:

function foo(arg0: string, arg1: number): void {
  // ...
}

foo("hello", 42);

foo("hello", 42, true); // Expected 2 arguments, but got 3.
foo("hello"); // Expected 2 arguments, but got 1.

但是,如果从代码可读性的角度来看,就能够看出两者之间的差别。 在第一个例子中,参数的第一个元素和第二个元素都没有参数名。 虽然这不影响类型检查,但是元组中元素位置上缺乏标签令它们难以使用 - 很难表达出代码的意图。

这就是为什么TypeScript 4.0中的元组可以提供标签。

type Range = [start: number, end: number];

为了加强参数列表和元组类型之间的联系,剩余元素和可选元素的语法采用了参数列表的语法。

type Foo = [first: number, second?: string, ...rest: any[]];

在使用标签元组时有一些规则要遵守。 其一是,如果一个元组元素使用了标签,那么所有元组元素必须都使用标签。

type Bar = [first: string, number];
// Tuple members must all have names or all not have names.(5084)

元组标签名不影响解构变量名,它们不必相同。 元组标签仅用于文档和工具目的。

function foo(x: [first: string, second: number]) {
    // ...

    // 注意:不需要命名为'first'和'second'
    const [a, b] = x;
    a
//  string
    b
//  number
}

总的来说,标签元组对于元组和参数列表模式以及实现类型安全的重载时是很便利的。 实际上,在代码编辑器中TypeScript会尽可能地将它们显示为重载。

Signature help displaying a union of labeled tuples as in a parameter list as two signatures

更多详情请参考PT

从构造函数中推断类属性

在TypeScript 4.0中,当启用了noImplicitAny时,编译器能够根据基于控制流的分析来确定类中属性的类型

class Square {
  // 在旧版本中,以下两个属性均为any类型
  area; // number
  sideLength; // number

  constructor(sideLength: number) {
    this.sideLength = sideLength;
    this.area = sideLength ** 2;
  }
}

如果没有在构造函数中的所有代码执行路径上为实例成员进行赋值,那么该属性会被认为可能为undefined类型。

class Square {
  sideLength; // number | undefined

  constructor(sideLength: number) {
    if (Math.random()) {
      this.sideLength = sideLength;
    }
  }

  get area() {
    return this.sideLength ** 2;
    //     ~~~~~~~~~~~~~~~
    //     对象可能为'undefined'
  }
}

如果你清楚地知道属性类型(例如,类中存在类似于initialize的初始化方法),你仍需要明确地使用类型注解来指定类型,以及需要使用确切赋值断言(!)如果你启用了strictPropertyInitialization模式。

class Square {
  // 确切赋值断言
  //        v
  sideLength!: number;
  //         ^^^^^^^^
  //         类型注解

  constructor(sideLength: number) {
    this.initialize(sideLength);
  }

  initialize(sideLength: number) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength ** 2;
  }
}

更多详情请参考PR.

断路赋值运算符

JavaScript以及其它很多编程语言支持一些_复合赋值_运算符。 复合赋值运算符作用于两个操作数,并将运算结果赋值给左操作数。 你从前可能见到过以下代码:

// 加
// a = a + b
a += b;

// 减
// a = a - b
a -= b;

// 乘
// a = a * b
a *= b;

// 除
// a = a / b
a /= b;

// 幂
// a = a ** b
a **= b;

// 左移位
// a = a << b
a <<= b;

JavaScript中的许多运算符都具有一个对应的赋值运算符! 目前为止,有三个值得注意的例外:逻辑_与_(&&),逻辑_或_(||)和逻辑_空值合并_(??)。

这就是为什么TypeScript 4.0支持了一个ECMAScript的新特性,增加了三个新的赋值运算符&&=||=??=

这三个运算符可以用于替换以下代码:

a = a && b;
a = a || b;
a = a ?? b;

或者相似的if语句

// could be 'a ||= b'
if (!a) {
  a = b;
}

还有以下的惰性初始化值的例子:

let values: string[];
(values ?? (values = [])).push("hello");

// After
(values ??= []).push("hello");

少数情况下当你使用带有副作用的存取器时,值得注意的是这些运算符只在必要时才执行赋值操作。 也就是说,不仅是运算符右操作数会“短路”,整个赋值操作也会“短路”

obj.prop ||= foo();

// roughly equivalent to either of the following

obj.prop || (obj.prop = foo());

if (!obj.prop) {
    obj.prop = foo();
}

尝试运行这个例子来查看与 _始终_执行赋值间的差别。

const obj = {
    get prop() {
        console.log("getter has run");

        // Replace me!
        return Math.random() < 0.5;
    },
    set prop(_val: boolean) {
        console.log("setter has run");
    }
};

function foo() {
    console.log("right side evaluated");
    return true;
}

console.log("This one always runs the setter");
obj.prop = obj.prop || foo();

console.log("This one *sometimes* runs the setter");
obj.prop ||= foo();

非常感谢社区成员Wenlu Wang为该功能的付出!

更多详情请参考PR. 你还可以查看该特性的TC39提案.

catch语句中的unknown类型

在TypeScript的早期版本中,catch语句中的捕获变量总为any类型。 这意味着你可以在捕获变量上执行任意的操作。

try {
  // Do some work
} catch (x) {
  // x 类型为 'any'
  console.log(x.message);
  console.log(x.toUpperCase());
  x++;
  x.yadda.yadda.yadda();
}

上述代码可能导致错误处理语句中产生了_更多_的错误,因此该行为是不合理的。 因为捕获变量默认为any类型,所以它不是类型安全的,你可以在上面执行非法操作。

TypeScript 4.0允许将catch语句中的捕获变量类型声明为unknown类型。 unknown类型比any类型更加安全,因为它要求在使用之前必须进行类型检查。

try {
  // ...
} catch (e: unknown) {
  // Can't access values on unknowns
  console.log(e.toUpperCase());

  if (typeof e === "string") {
    // We've narrowed 'e' down to the type 'string'.
    console.log(e.toUpperCase());
  }
}

由于catch语句捕获变量的类型不会被默认地改变成unknown类型,因此我们考虑在未来添加一个新的--strict标记来有选择性地引入该行为。 目前,我们可以通过使用代码静态检查工具来强制catch捕获变量使用了明确的类型注解: any: unknown

更多详情请参考PR.

自定义JSX工厂

在使用JSX时,fragment类型的JSX元素允许返回多个子元素。 当TypeScript刚开始实现fragments时,我们不太清楚其它代码库该如何使用它们。 最近越来越多的库开始使用JSX并支持与fragments结构相似的API。

在TypeScript 4.0中,用户可以使用jsxFragmentFactory选项来自定义fragment工厂。

例如,下例的tsconfig.json文件告诉TypeScript使用与React兼容的方式来转换JSX,但使用h来代替React.createElement工厂,同时使用Fragment来代替React.Fragment

{
  compilerOptions: {
    target: "esnext",
    module: "commonjs",
    jsx: "react",
    jsxFactory: "h",
    jsxFragmentFactory: "Fragment",
  },
}

如果针对每个文件具有不同的JSX工厂,你可以使用新的/** @jsxFrag */编译指令注释。 示例:

// 注意:这些编译指令注释必须使用JSDoc风格,否则不起作用

/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

export const Header = (
  <>
    <h1>Welcome</h1>
  </>
);

上述代码会转换为如下的JavaScript

// 注意:这些编译指令注释必须使用JSDoc风格,否则不起作用

/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

export const Header = (
  h(
    Fragment,
    null,
    h("h1", null, "Welcome")
  )
);

非常感谢社区成员Noj Vek为该特性的付出。

更多详情请参考PR

对启用了--noEmitOnError的`build模式进行速度优化

在以前,当启用了--noEmitOnError编译选项时,如果在--incremental构建模式下的前一次构建出错了,那么接下来的构建会很慢。 这是因为当启用了--noEmitOnError时,前一次失败构建的信息不会被缓存到.tsbuildinfo文件中。

TypeScript 4.0对此做出了一些改变,极大地提升了这种情况下的编译速度,改善了应用--build模式的场景(包含--incremental--noEmitOnError)。

更多详情请参考PR

--incremental--noEmit

TypeScript 4.0允许同时使用--incremental--noEmit。 这在之前是不允许的,因为--incremental需要生成.tsbuildinfo文件; 然而,提供更快地增量构建对所有用户来讲都是十分重要的。

更多详情请参考PR

编辑器改进

TypeScript编译器不但支持在大部分编辑器中编写TypeScript代码,还支持着在Visual Studio系列的编辑器中编写JavaScript代码。 因此,我们主要工作之一是改善编辑器支持 - 这也是程序员花费了大量时间的地方。

针对不同的编辑器,在使用TypeScript/JavaScript的新功能时可能会有所区别,但是

这里是支持TypeScript的编辑器列表,到这里查看你喜爱的编译器是否支持最新版本的TypeScript。

转换为可选链

可选链是一个较新的大家喜爱的特性。 TypeScript 4.0带来了一个新的重构工具来转换常见的代码模式,以利用可选链空值合并

将a && a.b.c && a.b.c.d.e.f()转换为a?.b.c?.d.e.f.()

注意,虽然该项重构不能_完美地_捕获真实情况(由于JavaScript中较复杂的真值/假值关系),但是我们坚信它能够适用于大多数使用场景,尤其是在TypeScript清楚地知道代码类型信息的时候。

更多详情请参考PR

/** @deprecated */支持

TypeScript现在能够识别代码中的/** @deprecated *JSDoc注释,并对编辑器提供支持。 该信息会显示在自动补全列表中以及建议诊断信息,编辑器可以特殊处理它。 在类似于VS Code的编辑器中,废弃的值会显示为删除线,例如like this

Some examples of deprecated declarations with strikethrough text in the editor

感谢Wenlu Wang为该特性的付出。 更多详情请参考PR

启动时的局部语义模式

我们从用户反馈得知在启动一个大的工程时需要很长的时间。 罪魁祸首是一个叫作_程序构造_的处理过程。 该处理是从一系列根文件开始解析并查找它们的依赖,然后再解析依赖,然后再解析依赖的依赖,以此类推。 你的工程越大,你等待的时间就越长,在这之前你不能使用编辑器的诸如“跳转到定义”等功能。

这就是为什么我们要提供一个新的编辑器模式,在语言服务被完全加载之前提供局部编辑体验。 这里的主要想法是,编辑器可以运行一个轻量级的局部语言服务,它只关注编辑器当前打开的文件。

很难准确地形容能够获得多大的提升,但听说在Visual Studio Code项目中,以前需要等待_20秒到1分钟_的时间来完全加载语言服务。 做为对比,新的局部语义模式看起来能够将上述时间减少到几秒钟 示例,从下面的视频中,你可以看到左侧的TypeScript 3.9与右侧的TypeScript 4.0的对比。

当在编辑器中打开一个大型的代码仓库时,TypeScript 3.9根本无法提供代码补全以及信息提示。 反过来,安装了TypeScript 4.0的编辑器能够在当前文件上_立即_提供丰富的编辑体验,尽管后台仍然在加载整个工程。

目前,唯一一个支持该模块的编辑器是Visual Studio Code,并且在Visual Studio Code Insiders版本中还带来了一些体验上的优化。 我们发现该特性在用户体验和功能性上仍有优化空间,我们总结了一个优化列表。 我们也期待你的使用反馈。

更多详情请参考原始的提议功能实现的PR,以及后续的跟踪帖.

更智能的自动导入

自动导入是个特别好的功能,它让编码更加容易;然而,每一次自动导入不好用的时候,它就会导致一部分用户流失。 一个特殊的问题是,自动导入对于使用TypeScript编写的依赖不好用 - 也就是说,用户必须在工程中的某处明确地编写一个导入语句。

那么为什么自动导入在@types包上是好用的,但是对于自己编写的代码却不好用? 这表明自动导入功能只适用于工程中已经引入的包。 因为TypeScript会自动地将node_modules/@types下面的包引入进工程,_那些_包才会被自动导入。 另一方面,其它的包会被排除,因为遍历node_modules下所有的包_相当_费时。

这就导致了在自动导入一个刚刚安装完但还没有开始使用的包时具有相当差的体验。

TypeScript 4.0对编辑器环境进行了一点小改动,它会自动引入你的工程下的package.json文件中dependencies(和peerDependencies)字段里列出的包。 这些引入的包只用于改进自动导入功能,它们对类型检查等其它功能没有任何影响。 这使得自动导入功能对于项目中所有带有类型的依赖项都是可用的,同时不必遍历node_modules

少数情况下,若在package.json中列出了多于10个未导入的带有类型的依赖,那么该功能会被自动禁用以避免过慢的工程加载。 若想要强制启用该功能,或完全禁用该功能,则需要配置你的编辑器。 针对Visual Studio Code,对应到“Include Package JSON Auto Imports”配置(或者typescript.preferences.includePackageJsonAutoImports配置)。

Configuring 'include package JSON auto imports' For more details, you can see the proposal issue along with the implementing pull request.

我们的新网站

最近,我们重写了TypeScript官网并且已经发布!

A screenshot of the new TypeScript website

我们在这里介绍了关于新网站的一些信息;但仍期望用户给予更多的反馈! 如果你有问题或建议,请到这里提交Issue

改进类型推断和Promise.all

TypeScript的最近几个版本(3.7前后)更新了像Promise.allPromise.race等的函数声明。 不巧的是,它引入了一些回归问题,尤其是在和nullundefined混合使用的场景中。

interface Lion {
  roar(): void;
}

interface Seal {
  singKissFromARose(): void;
}

async function visitZoo(
  lionExhibit: Promise<Lion>,
  sealExhibit: Promise<Seal | undefined>
) {
  let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);
  lion.roar();
  //   ~~~~
  //  对象可能为'undefined'
}

这是一种奇怪的行为! 事实上,只有sealExhibit包含了undefined值,但是它却让lion也含有了undefined值。

得益于Jack Bates提交的PR,这个问题已经被修复了,它改进了TypeScript 3.9中的类型推断流程。 上面的例子中已经不再产生错误。 如果你在旧版本的TypeScript中被Promise的这个问题所困扰,我们建议你尝试一下3.9版本!

awaited 类型

如果你一直关注TypeScript,那么你可能会注意到一个新的类型运算符awaited。 这个类型运算符的作用是准确地表达JavaScript中Promise的工作方式。

我们原计划在TypeScript 3.9中支持awaited,但在现有的代码中测试过该特性后,我们发现还需要进行一些设计,以便让所有人能够顺利地使用它。 因此,我们从主分支中暂时移除了这个特性。 我们将继续试验这个特性,它不会被包含进本次发布。

速度优化

TypeScript 3.9提供了多项速度优化。 TypeScript在material-uistyled-components代码包中拥有非常慢的编辑速度和编译速度。在发现了这点后,TypeScript团队集中了精力解决性能问题。 TypeScript优化了大型联合类型、交叉类型、有条件类型和映射类型。

  • https://github.com/microsoft/TypeScript/pull/36576
  • https://github.com/microsoft/TypeScript/pull/36590
  • https://github.com/microsoft/TypeScript/pull/36607
  • https://github.com/microsoft/TypeScript/pull/36622
  • https://github.com/microsoft/TypeScript/pull/36754
  • https://github.com/microsoft/TypeScript/pull/36696

上面列出的每一个PR都能够减少5-10%的编译时间(对于某些代码库)。 对于material-ui库而言,现在能够节约大约40%的编译时间!

我们还调整了在编辑器中的文件重命名功能。 从Visual Studio Code团队处得知,当重命名一个文件时,计算出需要更新的import语句要花费5到10秒的时间。 TypeScript 3.9通过改变编译器和语言服务缓存文件查询的内部实现解决了这个问题。

尽管仍有优化的空间,我们希望当前的改变能够为每个人带来更流畅的体验。

// @ts-expect-error 注释

设想一下,我们正在使用TypeScript编写一个代码库,它对外开放了一个公共函数doStuff。 该函数的类型声明了它接受两个string类型的参数,因此其它TypeScript的用户能够看到类型检查的结果,但该函数还进行了运行时的检查以便JavaScript用户能够看到一个有帮助的错误。

function doStuff(abc: string, xyz: string) {
  assert(typeof abc === "string");
  assert(typeof xyz === "string");

  // do some stuff
}

如果有人错误地使用了该函数,那么TypeScript用户能够看到红色的波浪线和错误提示,JavaScript用户会看到断言错误。 然后,我们想编写一条单元测试来测试该行为。

expect(() => {
  doStuff(123, 456);
}).toThrow();

不巧的是,如果你使用TypeScript来编译单元测试,TypeScript会提示一个错误!

doStuff(123, 456);
//      ~~~
// 错误:类型'number'不能够赋值给类型'string'。

这就是TypeScript 3.9添加了// @ts-expect-error注释的原因。 当一行代码带有// @ts-expect-error注释时,TypeScript不会提示上例的错误; 但如果该行代码没有错误,TypeScript会提示没有必要使用// @ts-expect-error

示例,以下的代码是正确的:

// @ts-expect-error
console.log(47 * "octopus");

但是下面的代码:

// @ts-expect-error
console.log(1 + 1);

会产生错误:

未使用的 '@ts-expect-error' 指令。

非常感谢Josh Goldberg实现了这个功能。 更多信息请参考the ts-expect-error pull request

ts-ignore 还是 ts-expect-error?

某些情况下,// @ts-expect-error// @ts-ignore是相似的,都能够阻止产生错误消息。 两者的不同在于,如果下一行代码没有错误,那么// @ts-ignore不会做任何事。

你可能会想要抛弃// @ts-ignore注释转而去使用// @ts-expect-error,并且想要知道哪一个更适用于以后的代码。 实际上,这完全取决于你和你的团队,下面列举了一些具体情况。

如果满足以下条件,那么选择ts-expect-error

  • 你在编写单元测试,并且想让类型系统提示错误
  • 你知道此处有问题,并且很快会回来改正它,只是暂时地忽略该错误
  • 你的团队成员都很积极,大家想要在代码回归正常后及时地删除忽略类型检查注释

如果满足以下条件,那么选择ts-ignore

  • 项目规模较大,产生了一些错误但是找不到相应代码的负责人
  • 正处于TypeScript版本升级的过程中,某些错误只在特定版本的TypeScript中存在,但是在其它版本中并不存在
  • 你没有足够的时间考虑究竟应该使用// @ts-ignore还是// @ts-expect-error

在条件表达式中检查未被调用的函数

在TypeScript 3.7中,我们引入了_未进行函数调用的检查_,当你忘记去调用某个函数时会产生错误。

function hasImportantPermissions(): boolean {
  // ...
}

// Oops!
if (hasImportantPermissions) {
  //  ~~~~~~~~~~~~~~~~~~~~~~~
  // 这个条件永远返回true,因为函数已经被定义。
  // 你是否想要调用该函数?
  deleteAllTheImportantFiles();
}

然而,这个错误只会在if条件语句中才会提示。 多亏了Alexander Tarasyuk提交的PR,现在这个特性也支持在三元表达式中使用,例如cond ? trueExpr : falseExpr

declare function listFilesOfDirectory(dirPath: string): string[];
declare function isDirectory(): boolean;

function getAllFiles(startFileName: string) {
  const result: string[] = [];
  traverse(startFileName);
  return result;

  function traverse(currentPath: string) {
    return isDirectory
      ? // ~~~~~~~~~~~
        // 该条件永远返回true
        // 因为函数已经被定义。
        // 你是否想要调用该函数?
        listFilesOfDirectory(currentPath).forEach(traverse)
      : result.push(currentPath);
  }
}

https://github.com/microsoft/TypeScript/issues/36048

编辑器改进

TypeScript编译器不但支持在大部分编辑器中编写TypeScript代码,还支持着在Visual Studio系列的编辑器中编写JavaScript代码。 针对不同的编辑器,在使用TypeScript/JavaScript的新功能时可能会有所区别,但是

在JavaScript中自动导入CommonJS模块

在使用了CommonJS模块的JavaScript文件中,我们对自动导入功能进行了一个非常棒的改进。

在旧的版本中,TypeScript总是假设你想要使用ECMAScript模块风格的导入语句,并且无视你的文件类型。

import * as fs from "fs";

然而,在编写JavaScript文件时,并不总是想要使用ECMAScript模块风格。 非常多的用户仍然在使用CommonJS模块,例如require(...)

const fs = require("fs");

现在,TypeScript会自动检测你正在使用的导入语句风格,并使用当前的导入语句风格。

更新信息请参考PR.

Code Actions 保留换行符

TypeScript的重构工具和快速修复工具对换行符的处理不是非常好。 一个基本的示例如下。

const maxValue = 100;

/*start*/
for (let i = 0; i <= maxValue; i++) {
  // First get the squared value.
  let square = i ** 2;

  // Now print the squared value.
  console.log(square);
}
/*end*/

如果我们选中从/*start*//*end*/,然后进行“提取到函数”操作,我们会得到如下的代码。

const maxValue = 100;

printSquares();

function printSquares() {
  for (let i = 0; i <= maxValue; i++) {
    // First get the squared value.
    let square = i ** 2;
    // Now print the squared value.
    console.log(square);
  }
}

在旧版本的TypeScript中,将循环提取到函数时,换行符没有被保留。

这不是我们想要的 - 在for循环中,每条语句之间都有一个空行,但是重构后它们被移除了! TypeScript 3.9调整后,它会保留我们编写的代码。

const maxValue = 100;

printSquares();

function printSquares() {
  for (let i = 0; i <= maxValue; i++) {
    // First get the squared value.
    let square = i ** 2;

    // Now print the squared value.
    console.log(square);
  }
}

在TypeScript 3.9中,将循环提取到函数时,会保留一个换行符。

更多信息请参考PR

快速修复:缺失的返回值表达式

有时候,我们可能忘记在函数的最后添加返回值语句,尤其是在将简单箭头函数转换成还有花括号的箭头函数时。

// before
let f1 = () => 42;

// oops - not the same!
let f2 = () => {
  42;
};

感谢开源社区的Wenlu WangPR,TypeScript提供了快速修复功能来添加return语句,删除花括号,或者为箭头函数体添加小括号用以区分对象字面量。

示例

支持"Solution Style"的tsconfig.json文件

编译器需要知道一个文件被哪个配置文件所管理,因此才能够应用适当的配置选项并且计算出当前“工程”包含了哪些文件。 在默认情况下,编辑器使用TypeScript语言服务来向上遍历父级目录以查找tsconfig.json文件。

有一种特殊情况是tsconfig.json文件仅用于引用其它tsconfig.json文件。

// tsconfig.json
{
  files: [],
  references: [
    { path: "./tsconfig.shared.json" },
    { path: "./tsconfig.frontend.json" },
    { path: "./tsconfig.backend.json" },
  ],
}

这个文件除了用来管理其它项目的配置文件之外什么也没做,在某些环境中它被叫作“solution”。 这里,任何一个tsconfig.*.json文件都不会被TypeScript语言服务所选用,但是我们希望语言服务能够分析出当前的.ts文件被上述tsconfig.json中引用的哪个配置文件所管理。

TypeScript 3.9为这种类型的配置方式添加了编辑器的支持。 更多信息请参考PR.

TypeScript 3.8

类型导入和导出(Type-Only Imports and Exports)

This feature is something most users may never have to think about; however, if you've hit issues under --isolatedModules, TypeScript's transpileModule API, or Babel, this feature might be relevant.

TypeScript 3.8 adds a new syntax for type-only imports and exports.

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import type only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there's no remnant of it at runtime. Similarly, export type only provides an export that can be used for type contexts, and is also erased from TypeScript's output.

It's important to note that classes have a value at runtime and a type at design-time, and the use is context-sensitive. When using import type to import a class, you can't do things like extend from it.

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

If you've used Flow before, the syntax is fairly similar. One difference is that we've added a few restrictions to avoid code that might appear ambiguous.

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

In conjunction with import type, TypeScript 3.8 also adds a new compiler flag to control what happens with imports that won't be utilized at runtime: importsNotUsedAsValues. This flag takes 3 different values:

  • remove: this is today's behavior of dropping these imports. It's going to continue to be the default, and is a non-breaking change.
  • preserve: this preserves all imports whose values are never used. This can cause imports/side-effects to be preserved.
  • error: this preserves all imports (the same as the preserve option), but will error when a value import is only used as a type. This might be useful if you want to ensure no values are being accidentally imported, but still make side-effect imports explicit.

For more information about the feature, you can take a look at the pull request, and relevant changes around broadening where imports from an import type declaration can be used.

ECMAScript 私有变量(ECMAScript Private Fields

TypeScript 3.8 brings support for ECMAScript's private fields, part of the stage-3 class fields proposal.

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

Unlike regular properties (even ones declared with the private modifier), private fields have a few rules to keep in mind. Some of them are:

  • Private fields start with a # character. Sometimes we call these private names.
  • Every private field name is uniquely scoped to its containing class.
  • TypeScript accessibility modifiers like public or private can't be used on private fields.
  • Private fields can't be accessed or even detected outside of the containing class - even by JS users! Sometimes we call this hard privacy.

Apart from "hard" privacy, another benefit of private fields is that uniqueness we just mentioned. For example, regular property declarations are prone to being overwritten in subclasses.

class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

With private fields, you'll never have to worry about this, since each field name is unique to the containing class.

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

Another thing worth noting is that accessing a private field on any other type will result in a TypeError!

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

Finally, for any plain .js file users, private fields always have to be declared before they're assigned to.

class C {
    // No declaration for '#foo'
    // :(

    constructor(foo: number) {
        // SyntaxError!
        // '#foo' needs to be declared before writing to it.
        this.#foo = foo;
    }
}

JavaScript has always allowed users to access undeclared properties, whereas TypeScript has always required declarations for class properties. With private fields, declarations are always needed regardless of whether we're working in .js or .ts files.

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}

For more information about the implementation, you can check out the original pull request

Which should I use?

We've already received many questions on which type of privates you should use as a TypeScript user: most commonly, "should I use the private keyword, or ECMAScript's hash/pound (#) private fields?" It depends!

When it comes to properties, TypeScript's private modifiers are fully erased - that means that at runtime, it acts entirely like a normal property and there's no way to tell that it was declared with a private modifier. When using the private` keyword, privacy is only enforced at compile-time/design-time, and for JavaScript consumers it's entirely intent-based.

class C {
    private foo = 10;
}

// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo);    // prints '10'
//                  ~~~
// error! Property 'foo' is private and only accessible within class 'C'.

// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

The upside is that this sort of "soft privacy" can help your consumers temporarily work around not having access to some API, and also works in any runtime.

On the other hand, ECMAScript's # privates are completely inaccessible outside of the class.

class C {
    #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!

console.log(new C()["#foo"]); // prints undefined
//          ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

This hard privacy is really useful for strictly ensuring that nobody can take use of any of your internals. If you're a library author, removing or renaming a private field should never cause a breaking change.

As we mentioned, another benefit is that subclassing can be easier with ECMAScript's # privates because they really are private. When using ECMAScript # private fields, no subclass ever has to worry about collisions in field naming. When it comes to TypeScript's private property declarations, users still have to be careful not to trample over properties declared in superclasses.

One more thing to think about is where you intend for your code to run. TypeScript currently can't support this feature unless targeting ECMAScript 2015 (ES6) targets or higher. This is because our downleveled implementation uses WeakMaps to enforce privacy, and WeakMaps can't be polyfilled in a way that doesn't cause memory leaks. In contrast, TypeScript's private-declared properties work with all targets - even ECMAScript 3!

A final consideration might be speed: private properties are no different from any other property, so accessing them is as fast as any other property access no matter which runtime you target. In contrast, because # private fields are downleveled using WeakMaps, they may be slower to use. While some runtimes might optimize their actual implementations of # private fields, and even have speedy WeakMap implementations, that might not be the case in all runtimes.

export * as ns Syntax

It's often common to have a single entry-point that exposes all the members of another module as a single member.

import * as utilities from "./utilities.js";
export { utilities };

This is so common that ECMAScript 2020 recently added a new syntax to support this pattern!

export * as utilities from "./utilities.js";

This is a nice quality-of-life improvement to JavaScript, and TypeScript 3.8 implements this syntax. When your module target is earlier than es2020, TypeScript will output something along the lines of the first code snippet.

顶层await(Top-Level await)

TypeScript 3.8 provides support for a handy upcoming ECMAScript feature called "top-level await".

JavaScript users often introduce an async function in order to use await, and then immediately called the function after defining it.

async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}

main()
    .catch(e => console.error(e))

This is because previously in JavaScript (along with most other languages with a similar feature), await was only allowed within the body of an async function. However, with top-level await, we can use await at the top level of a module.

const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

Note there's a subtlety: top-level await only works at the top level of a module, and files are only considered modules when TypeScript finds an import or an export. In some basic cases, you might need to write out export {} as some boilerplate to make sure of this.

Top level await may not work in all environments where you might expect at this point. Currently, you can only use top level await when the target compiler option is es2017 or above, and module is esnext or system. Support within several environments and bundlers may be limited or may require enabling experimental support.

For more information on our implementation, you can check out the original pull request.

es2020 for target and module

TypeScript 3.8 supports es2020 as an option for module and target. This will preserve newer ECMAScript 2020 features like optional chaining, nullish coalescing, export * as ns, and dynamic import(...) syntax. It also means bigint literals now have a stable target below esnext.

JSDoc 属性修饰词(JSDoc Property Modifiers)

TypeScript 3.8 supports JavaScript files by turning on the allowJs flag, and also supports type-checking those JavaScript files via the checkJs option or by adding a // @ts-check comment to the top of your .js files.

Because JavaScript files don't have dedicated syntax for type-checking, TypeScript leverages JSDoc. TypeScript 3.8 understands a few new JSDoc tags for properties.

First are the accessibility modifiers: @public, @private, and @protected. These tags work exactly like public, private, and protected respectively work in TypeScript.

// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
  • @public 是默认的,可以省略,它代表了一个属性可以从任何地方访问它
  • @private 表示一个属性只能在包含的类中访问
  • @protected 表示该属性只能在所包含的类及子类中访问,但不能在类的实例中访问

下一步,我们计划添加 @readonly 修饰符,来确保一个属性只能在初始化时被修改:

// @ts-check

class Foo {
    constructor() {
        /** @readonly */
        this.stuff = 100;
    }

    writeToStuff() {
        this.stuff = 200;
        //   ~~~~~
        // Cannot assign to 'stuff' because it is a read-only property.
    }
}

new Foo().stuff++;
//        ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

Better Directory Watching on Linux and watchOptions

TypeScript 3.8 ships a new strategy for watching directories, which is crucial for efficiently picking up changes to node_modules.

For some context, on operating systems like Linux, TypeScript installs directory watchers (as opposed to file watchers) on node_modules and many of its subdirectories to detect changes in dependencies. This is because the number of available file watchers is often eclipsed by the of files in node_modules, whereas there are way fewer directories to track.

Older versions of TypeScript would immediately install directory watchers on folders, and at startup that would be fine; however, during an npm install, a lot of activity will take place within node_modules and that can overwhelm TypeScript, often slowing editor sessions to a crawl. To prevent this, TypeScript 3.8 waits slightly before installing directory watchers to give these highly volatile directories some time to stabilize.

Because every project might work better under different strategies, and this new approach might not work well for your workflows, TypeScript 3.8 introduces a new watchOptions field in tsconfig.json and jsconfig.json which allows users to tell the compiler/language service which watching strategies should be used to keep track of files and directories.

{
    // Some typical compiler options
    "compilerOptions": {
        "target": "es2020",
        "moduleResolution": "node",
        // ...
    },

    // NEW: Options for file/directory watching
    "watchOptions": {
        // Use native file system events for files and directories
        "watchFile": "useFsEvents",
        "watchDirectory": "useFsEvents",

        // Poll files for updates more frequently
        // when they're updated a lot.
        "fallbackPolling": "dynamicPriority"
    }
}

watchOptions 包含四种新的选项:

  • watchFile: 监听单个文件的策略,它可以有以下值
    • fixedPollingInterval: 以固定的时间间隔,检查文件的更改
    • priorityPollingInterval: 以固定的时间间隔,检查文件的更改,但是使用「heuristics」检查某些类型的文件的频率比其他文件低(heuristics 怎么翻?)
    • dynamicPriorityPolling: 使用动态队列,在该队列中,较少检查不经常修改的文件
    • useFsEvents (默认): 尝试使用操作系统/文件系统原生事件来监听文件更改
    • useFsEventsOnParentDirectory: 尝试使用操作系统/文件系统原生事件来监听文件、目录的更改,这样可以使用较小的文件监听程序,但是准确性可能较低
  • watchDirectory: 在缺少递归文件监听功能的系统中,使用哪种策略监听整个目录树,它可以有以下值 :
    • fixedPollingInterval: 以固定的时间间隔,检查目录树的更改
    • dynamicPriorityPolling: 使用动态队列,在该队列中,较少检查不经常修改的目录
    • useFsEvents (默认): 尝试使用操作系统/文件系统原生事件来监听目录更改
  • fallbackPolling: 当使用文件系统的事件,该选项用来指定使用特定策略,它可以有以下值
    • fixedPollingInterval: (同上)
    • priorityPollingInterval: (同上)
    • dynamicPriorityPolling: (同上)
  • synchronousWatchDirectory: 在目录上禁用延迟监听功能。在可能一次发生大量文件(如 node_modules)更改时,它非常有用,但是你可能需要一些不太常见的设置时,禁用它。

For more information on these changes, head over to GitHub to see the pull request to read more.

"Fast and Loose" Incremental Checking

TypeScript 3.8 introduces a new compiler option called assumeChangesOnlyAffectDirectDependencies. When this option is enabled, TypeScript will avoid rechecking/rebuilding all truly possibly-affected files, and only recheck/rebuild files that have changed as well as files that directly import them.

For example, consider a file fileD.ts that imports fileC.ts that imports fileB.ts that imports fileA.ts as follows:

fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts

In --watch mode, a change in fileA.ts would typically mean that TypeScript would need to at least re-check fileB.ts, fileC.ts, and fileD.ts. Under assumeChangesOnlyAffectDirectDependencies, a change in fileA.ts means that only fileA.ts and fileB.ts need to be re-checked.

In a codebase like Visual Studio Code, this reduced rebuild times for changes in certain files from about 14 seconds to about 1 second. While we don't necessarily recommend this option for all codebases, you might be interested if you have an extremely large codebase and are willing to defer full project errors until later (e.g. a dedicated build via a tsconfig.fullbuild.json or in CI).

For more details, you can see the original pull request.

TypeScript 3.7

可选链(Optional Chaining)

Playground

在我们的 issue 列表上,可选链是 issue #16。感受一下,从那之后 TypeScript 的 issue 列表中新增了 23,000 条 issues。

可选链的核心是,在我们编写代码中,当遇到 nullundefined,TypeScript 可以立即停止解析一部分表达式。 可选链的关键点是一个为 可选属性访问 提供的新的运算符 ?.。 比如我们可以这样写代码:

let x = foo?.bar.baz();

意思是,当 foo 有定义时,执行 foo.bar.baz() 的计算;但是当 foonullundefined 时,停止后续的解析,直接返回 undefined

更明确地说,上面的代码和下面的代码等价。

let x = (foo === null || foo === undefined) ?
    undefined :
    foo.bar.baz();

注意,当 barnullundefined,我们的代码访问 baz 依然会报错。 同理,当 baznullundefined,在调用时也会报错。 ?. 只检查它 左边 的值是不是 nullundefined,不检查后续的属性。

你会发现自己可以使用 ?. 来替换用了 && 的大量空值检查代码。

// 以前
if (foo && foo.bar && foo.bar.baz) {
    // ...
}

// 以后
if (foo?.bar?.baz) {
    // ...
}

注意,?.&& 的行为略有不同,因为 && 会作用在所有“假”值上(例如,空字符串、0NaN 以及 false),但 ?. 是一个仅作用于结构上的特性。 它不会在有效数据(比如 0 或空字符串)上进行短路计算。

可选链还包括两个另外的用法。 首先是 可选元素访问,表现类似于可选属性访问,但是也允许我们访问非标识符属性(例如:任意字符串、数字和 symbol):

/**
 * 如果 arr 是一个数组,返回第一个元素
 * 否则返回 undefined
 */
function tryGetFirstElement<T>(arr?: T[]) {
    return arr?.[0];
    // 等价于:
    //   return (arr === null || arr === undefined) ?
    //       undefined :
    //       arr[0];
}

另一个是 可选调用,判断条件是当该表达式不是 nullundefined,我们就可以调用它。

async function makeRequest(url: string, log?: (msg: string) => void) {
    log?.(`Request started at ${new Date().toISOString()}`);
    // 基本等价于:
    //   if (log != null) {
    //       log(`Request started at ${new Date().toISOString()}`);
    //   }

    const result = (await fetch(url)).json();

    log?.(`Request finished at at ${new Date().toISOString()}`);

    return result;
}

可选链的“短路计算”行为仅限于属性访问、调用、元素访问——它不会延伸到后续的表达式中。 也就是说,

let result = foo?.bar / someComputation()

可选链不会阻止除法运算或 someComputation() 的进行。 上面这段代码实际上等价于:

let temp = (foo === null || foo === undefined) ?
    undefined :
    foo.bar;

let result = temp / someComputation();

当然,这可能会使得 undefined 参与了除法运算,导致在 strictNullChecks 编译选项下产生报错。

function barPercentage(foo?: { bar: number }) {
    return foo?.bar / 100;
    //     ~~~~~~~~
    // Error: Object is possibly undefined.
}

想了解更多细节,你可以 检阅完整的草案 以及 查看原始的 PR

空值合并(Nullish Coalescing)

Playground

空值合并运算符 是另一个即将到来的 ECMAScript 特性(与可选链一起),我们的团队也参与了 TC39 的的讨论工作。

你可以考虑使用 ?? 运算符来实现:当字段是 nullundefined 时,“回退”到默认值。 比如我们可以这样写代码:

let x = foo ?? bar();

这种新方式的意思是,当 foo “存在”时 x 等于 foo; 但假如 foonullundefined ,x 等于 bar() 的计算结果。

同样的,上面的代码可以写出等价代码。

let x = (foo !== null && foo !== undefined) ?
    foo :
    bar();

当尝试使用默认值时,?? 运算符可以代替 || 的作用。 例如,下面的代码片段尝试获取上一次储存在 localStorage 中的 volume(如果它已保存); 但是因为使用了 || ,留下一个 bug。

function initializeAudio() {
    let volume = localStorage.volume || 0.5

    // ...
}

如果 localStorage.volume 的值是 0,这段代码将会把 volume 的值设置为 0.5,这是一个意外情况。 而 ?? 避免了将 0NaN"" 视为假值的意外情况。

我们非常感谢社区成员 Wenlu WangTitian Cernicova Dragomir 实现了这个特性! 想了解更多细节,你可以 查看他们的 PR空值合并草案的 Repo

断言函数

Playground

有一类特定的函数,用于在出现非预期结果时抛出一个错误。 这样的函数叫做“断言”函数(Assertion Function)。 比方说,Node.js 中就有一个名为 assert 的断言函数。

assert(someValue === 42);

在上面的例子中,如果 someValue 不等于 42,那么 assert 就会抛出一个 AssertionError 错误。

在 JavaScript 中,断言经常被用于防止不正确传参。 举个例子:

function multiply(x, y) {
    assert(typeof x === "number");
    assert(typeof y === "number");

    return x * y;
}

很遗憾,在 TypeScript 中,这些检查没办法正确编码。 对于类型宽松的代码,意味着 TypeScript 检查得更少,而对于更加规范的代码,通常迫使使用者添加类型断言。

function yell(str) {
    assert(typeof str === "string");

    return str.toUppercase();
    // 糟了!我们拼错了 'toUpperCase'。
    // 如果 TypeScript 依然能检查出来就太棒了!
}

有一个替代的写法,可以让 TypeScript 能够分析出问题,不过这样并不方便。

function yell(str) {
    if (typeof str !== "string") {
        throw new TypeError("str should have been a string.")
    }
    // 发现错误!
    return str.toUppercase();
}

归根结底,TypeScript 的目标是以最小的改动为现存的 JavaScript 结构添加上类型声明。 因此,TypeScript 3.7 引入了一个称为“断言签名”的新概念,用于模拟这些断言函数。

第一种断言签名模拟了 Node 中 assert 函数的功能。 它确保在断言的范围内,无论什么判断条件都为必须真。

function assert(condition: any, msg?: string): asserts condition {
    if (!condition) {
        throw new AssertionError(msg)
    }
}

asserts condition 表示:如果 assert 函数成功返回,则传入的 condition 参数必须为真(否则它应该抛出一个 Error)。 这意味着对于同作用域中的后续代码,条件必须为真。 回到例子上,用这个断言函数意味着我们 能够 捕获之前 yell 示例中的错误。

function yell(str) {
    assert(typeof str === "string");

    return str.toUppercase();
    //         ~~~~~~~~~~~
    // error: Property 'toUppercase' does not exist on type 'string'.
    //        Did you mean 'toUpperCase'?
}

function assert(condition: any, msg?: string): asserts condition {
    if (!condition) {
        throw new AssertionError(msg)
    }
}

另一种类型的断言签名不通过检查条件语句实现,而是在 TypeScript 里显式指定某个变量或属性具有不同的类型。

function assertIsString(val: any): asserts val is string {
    if (typeof val !== "string") {
        throw new AssertionError("Not a string!");
    }
}

这里的 asserts val is string 保证了在 assertIsString 调用之后,传入的任何变量都有可以被视为是 string 类型的。

function yell(str: any) {
    assertIsString(str);

    // 现在 TypeScript 知道 'str' 是一个 'string'。

    return str.toUppercase();
    //         ~~~~~~~~~~~
    // error: Property 'toUppercase' does not exist on type 'string'.
    //        Did you mean 'toUpperCase'?
}

这些断言方法签名类似于类型谓词(type predicate)签名:

function isString(val: any): val is string {
    return typeof val === "string";
}

function yell(str: any) {
    if (isString(str)) {
        return str.toUppercase();
    }
    throw "Oops!";
}

就像类型谓词签名一样,这些断言签名具有清晰的表现力。 我们可以用它们表达一些非常复杂的想法。

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
    if (val === undefined || val === null) {
        throw new AssertionError(
            `Expected 'val' to be defined, but received ${val}`
        );
    }
}

想了解更多断言签名的细节,可以 查看原始的 PR

更好地支持返回 never 的函数

作为断言签名实现的一部分,TypeScript 需要编码更多关于调用位置和调用函数的细节。 这给了我们机会扩展对另一类函数的支持——返回 never 的函数。

返回 never 的函数,即永远不会返回的函数。 它表明抛出了异常、触发了停止错误条件、或程序退出的情况。 例如,@types/node 中的 process.exit(...) 就被指定为返回 never

为了确保函数永远不会潜在地返回 undefined、或者从所有代码路径中有效地返回,TypeScript 需要借助一些语法标志——函数结尾处的 returnthrow。 这样,使用者就会发现自己的代码在“返回”一个停机函数。

function dispatch(x: string | number): SomeType {
    if (typeof x === "string") {
        return doThingWithString(x);
    }
    else if (typeof x === "number") {
        return doThingWithNumber(x);
    }
    return process.exit(1);
}

现在,这些返回 never 的函数被调用时,TypeScript 能识别出它们将影响代码执行流程,同时说明原因。

function dispatch(x: string | number): SomeType {
    if (typeof x === "string") {
        return doThingWithString(x);
    }
    else if (typeof x === "number") {
        return doThingWithNumber(x);
    }
    process.exit(1);
}

你可以和在断言函数的 同一个 PR 中查看更多细节

(更加)递归的类型别名

Playground

类型别名在“递归”引用方面一直存在局限性。 原因是,类型别名必须能用它代表的东西来代替自己。 这在某些情况下是不可能的,因此编译器会拒绝某些递归别名,比如下面这个:

type Foo = Foo;

这是一个合理的限制,因为任何对 Foo 的使用都可以替换为 Foo,同时这个 Foo 能够替换为 Foo,而这个 Foo 应该……(产生了无限循环)希望你理解到这个意思了! 到最后,没有类型可以用来代替 Foo

其他语言也是这么处理类型别名的,但是它确实会产生一些令人困惑的情形,影响类型别名的使用。 例如,在 TypeScript 3.6 和更低的版本中,下面的代码会报错:

type ValueOrArray<T> = T | Array<ValueOrArray<T>>;
//   ~~~~~~~~~~~~
// error: Type alias 'ValueOrArray' circularly references itself.

这很令人困惑,因为使用者总是可以用接口来编写具有相同作用的代码,那么从技术上讲这没什么问题。

type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;

interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}

因为接口(以及其他对象 type)引入了一个间接的层级,并且它们的完整结构不需要立即建立,所以 TypeScript 可以处理这种结构。

但是,对于使用者而言,引入接口的方案并不直观。 并且,用了 Array 的初始版 ValueOrArray 没什么原则性问题。 如果编译器多一点“惰性”,并且只按需计算 Array 的类型参数,那么 TypeScript 就可以正确地表示出这些了。

这正是 TypeScript 3.7 引入的。 在类型别名的“顶层”,TypeScript 将推迟解析类型参数以便支持这些模式。

这意味着,用于表示 JSON 的以下代码……

type Json =
    | string
    | number
    | boolean
    | null
    | JsonObject
    | JsonArray;

interface JsonObject {
    [property: string]: Json;
}

interface JsonArray extends Array<Json> {}

终于可以重写成不需要借助 interface 的形式。

type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json }
    | Json[];

这个新的机制让我们在元组中,同样也可以递归地使用类型别名。 下面的 TypeScript 代码在以前会报错,但现在是合法的:

type VirtualNode =
    | string
    | [string, { [key: string]: any }, ...VirtualNode[]];

const myNode: VirtualNode =
    ["div", { id: "parent" },
        ["div", { id: "first-child" }, "I'm the first child"],
        ["div", { id: "second-child" }, "I'm the second child"]
    ];

想了解更多细节,你可以 查看原始的 PR

--declaration--allowJs

--declaration 选项允许我们从 TypeScript 源文件(诸如 .ts.tsx 文件)生成 .d.ts 文件(声明文件)。 .d.ts 文件的重要性有几个方面:

首先,它们使得 TypeScript 能够对外部项目进行类型检查,同时避免重复检查其源代码。 另一方面,它们使得 TypeScript 能够与现存的 JavaScript 库相互配合,即使这些库构建时并未使用 TypeScript。 最后,还有一个通常被忽略的好处:在使用支持 TypeScript 的编辑器时,TypeScript JavaScript 使用者都可以从这些文件中受益,例如更高级的自动完成。

不幸的是,--declaration 不能与 --allowJs 选项一起使用,--allowJs 选项允许混合使用 TypeScript 和 JavaScript 文件。 这是一个令人沮丧的限制,因为它意味着使用者在迁移代码库时无法使用 --declaration 选项,即使代码包含了 JSDoc 注释。 TypeScript 3.7 对此进行了改进,允许这两个选项一起使用!

这个功能最大的影响可能比较微妙:在 TypeScript 3.7 中,编写带有 JSDoc 注释的 JavaScript 库,也能帮助 TypeScript 的使用者。

它的实现原理是,在启用 allowJs 时,TypeScript 会尽可能地分析并理解常见的 JavaScript 模式;然而,用 JavaScript 表达的某些模式看起来不一定像它们在 TypeScript 中的等效形式。 启用 declaration 选项后,TypeScript 会尽力识别 JSDoc 注释和 CommonJS 形式的模块输出,并转换为有效的类型声明输出到 .d.ts 文件上。

比如下面这个代码片段

const assert = require("assert")

module.exports.blurImage = blurImage;

/**
 * Produces a blurred image from an input buffer.
 * 
 * @param input {Uint8Array}
 * @param width {number}
 * @param height {number}
 */
function blurImage(input, width, height) {
    const numPixels = width * height * 4;
    assert(input.length === numPixels);
    const result = new Uint8Array(numPixels);

    // TODO

    return result;
}

将会生成如下 .d.ts 文件

/**
 * Produces a blurred image from an input buffer.
 *
 * @param input {Uint8Array}
 * @param width {number}
 * @param height {number}
 */
export function blurImage(input: Uint8Array, width: number, height: number): Uint8Array;

除了基本的带有 @param 标记的函数,也支持其他情形, 请看下面这个例子:

/**
 * @callback Job
 * @returns {void}
 */

/** Queues work */
export class Worker {
    constructor(maxDepth = 10) {
        this.started = false;
        this.depthLimit = maxDepth;
        /**
         * NOTE: queued jobs may add more items to queue
         * @type {Job[]}
         */
        this.queue = [];
    }
    /**
     * Adds a work item to the queue
     * @param {Job} work 
     */
    push(work) {
        if (this.queue.length + 1 > this.depthLimit) throw new Error("Queue full!");
        this.queue.push(work);
    }
    /**
     * Starts the queue if it has not yet started
     */
    start() {
        if (this.started) return false;
        this.started = true;
        while (this.queue.length) {
            /** @type {Job} */(this.queue.shift())();
        }
        return true;
    }
}

会生成如下 .d.ts 文件:

/**
 * @callback Job
 * @returns {void}
 */
/** Queues work */
export class Worker {
    constructor(maxDepth?: number);
    started: boolean;
    depthLimit: number;
    /**
     * NOTE: queued jobs may add more items to queue
     * @type {Job[]}
     */
    queue: Job[];
    /**
     * Adds a work item to the queue
     * @param {Job} work
     */
    push(work: Job): void;
    /**
     * Starts the queue if it has not yet started
     */
    start(): boolean;
}
export type Job = () => void;

注意,当同时启用这两个选项时,TypeScript 不一定必须得编译成 .js 文件。 如果只是简单的想让 TypeScript 创建 .d.ts 文件,你可以启用 --emitDeclarationOnly 编译选项。

想了解更多细节,你可以 查看原始的 PR

useDefineForClassFields 编译选项和 declare 属性修饰符

当在 TypeScript 中写类公共字段时,我们尽力保证以下代码

class C {
    foo = 100;
    bar: string;
}

等价于构造函数中的相似语句

class C {
    constructor() {
        this.foo = 100;
    }
}

不幸的是,虽然这符合该提案早期的发展方向,但类公共字段极有可能以不同的方式进行标准化。 所以取而代之的,原始代码示例可能需要进行脱糖处理,变成类似下面的代码:

class C {
    constructor() {
        Object.defineProperty(this, "foo", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 100
        });
        Object.defineProperty(this, "bar", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
    }
}

当然,TypeScript 3.7 在默认情况下的编译结果与之前版本没有变化,我们增量地发布改动,以便帮助使用者减少未来潜在的破坏性变更。 我们提供了一个新的编译选项 useDefineForClassFields,根据一些新的检查逻辑使用上面这种编译模式。

最大的两个改变如下:

  • 声明通过 Object.defineProperty 完成。
  • 声明 总是 被初始化为 undefined,即使原有代码中没有显式的初始值。

对于现存的含有继承的代码,这可能会造成一些问题。首先,基类的 set 访问器不再被触发——它们将被完全覆写。

class Base {
    set data(value: string) {
        console.log("data changed to " + value);
    }
}

class Derived extends Base {
    // 当启用 'useDefineForClassFields' 时
    // 不再触发 'console.log'
    data = 10;
}

其次,基类中的属性设定也将不起作用。

interface Animal { animalStuff: any }
interface Dog extends Animal { dogStuff: any }

class AnimalHouse {
    resident: Animal;
    constructor(animal: Animal) {
        this.resident = animal;
    }
}

class DogHouse extends AnimalHouse {
    // 当启用 'useDefineForClassFields' 时
    // 调用 'super()' 后
    // 'resident' 只会被初始化成 'undefined'!
    resident: Dog;

    constructor(dog: Dog) {
        super(dog);
    }
}

这两个问题归结为,继承时混合覆写属性与访问器,以及属性不带初始值的重新声明。

为了检测这个访问器的问题,TypeScript 3.7 现在可以在 .d.ts 文件中编译出 get/set,这样 TypeScript 就能检查出访问器覆写的情况。

对于改变类字段的代码,将字段初始化写成构造函数内的语句,就可以解决此问题。

class Base {
    set data(value: string) {
        console.log("data changed to " + value);
    }
}

class Derived extends Base {
    constructor() {
        data = 10;
    }
}

而解决第二个问题,你可以显式地提供一个初始值,或添加一个declare 修饰符来表示这个属性不要被编译。

interface Animal { animalStuff: any }
interface Dog extends Animal { dogStuff: any }

class AnimalHouse {
    resident: Animal;
    constructor(animal: Animal) {
        this.resident = animal;
    }
}

class DogHouse extends AnimalHouse {
    declare resident: Dog;
//  ^^^^^^^
// 'resident' now has a 'declare' modifier,
// and won't produce any output code.

    constructor(dog: Dog) {
        super(dog);
    }
}

目前,只有当编译目标是 ES5 及以上时 useDefineForClassFields 才可用,因为 ES3 中不支持 Object.defineProperty。 要检查类似的问题,你可以创建一个分离的项目,设定编译目标为 ES5 并使用 --noEmit 来避免完全构建。

想了解更多细节,你可以 去原始的 PR 查看这些改动

我们强烈建议使用者尝试 useDefineForClassFields,并在 issues 或下面的评论区域中提供反馈。 应该碰到编译选项在使用难度上的反馈,这样我们就能够了解如何使迁移变得更容易。

利用项目引用实现无构建编辑

TypeScript 的项目引用功能,为我们提供了一种简单的方法来分解代码库,从而使编译速度更快。 遗憾的是,当我们编辑一个依赖未曾构建(或者构建结果过时)的项目时,体验不好。

在 TypeScript 3.7 中,当打开一个带有依赖的项目时,TypeScript 将自动切换为使用依赖中的 .ts/.tsx 源码文件。 这意味着在带有外部引用的项目中,代码的修改会即时同步和生效,编码体验会得到提升。 你也可以适当地打开编译器选项 disableSourceOfProjectReferenceRedirect 来禁用这个引用的功能,因为在超大型项目中这个功能可能会影响性能。

你可以 阅读这个 PR 来了解这个改动的更多细节

检查未调用的函数

一个常见且危险的错误是:忘记调用一个函数,特别是当该函数不需要参数,或者它的命名容易被误认为是一个属性而不是函数时。

interface User {
    isAdministrator(): boolean;
    notify(): void;
    doNotDisturb?(): boolean;
}

// 之后…

// 有问题的代码,别用!
function doAdminThing(user: User) {
    // 糟了!
    if (user.isAdministrator) {
        sudo();
        editTheConfiguration();
    }
    else {
        throw new AccessDeniedError("User is not an admin");
    }
}

在这段代码中,我们忘了调用 isAdministrator,导致该代码错误地允许非管理员用户修改配置!

在 TypeScript 3.7 中,它会被识别成一个潜在的错误:

function doAdminThing(user: User) {
    if (user.isAdministrator) {
    //  ~~~~~~~~~~~~~~~~~~~~
    // error! This condition will always return true since the function is always defined.
    //        Did you mean to call it instead?

这个检查功能是一个破坏性变更,基于这个因素,检查会非常保守。 因此对这类错误的提示仅限于 if 条件语句中。当问题函数是可选属性、或未开启 strictNullChecks 选项、或该函数在 if 的代码块中有被调用,在这些情况下不会被视为错误:

interface User {
    isAdministrator(): boolean;
    notify(): void;
    doNotDisturb?(): boolean;
}

function issueNotification(user: User) {
    if (user.doNotDisturb) {
        // OK,属性是可选的
    }
    if (user.notify) {
        // OK,调用了该函数
        user.notify();
    }
}

如果你打算对该函数进行测试但不调用它,你可以修改它的类型定义,让它可能是 undefined/null,或使用 !! 来编写类似 if (!!user.isAdministrator) 的代码,表示代码逻辑确实是这样的。

我们非常感谢社区成员 @jwbay 提出了 这个问题的概念 并持续跟进实现了 这个需求的当前版本

TypeScript 文件中的 // @ts-nocheck

TypeScript 3.7 允许我们在 TypeScript 文件的顶部添加一行 // @ts-nocheck 注释来关闭语义检查。 这个注释原本只在 checkJs 选项启用时的 JavaScript 源文件中有效,但我们扩展了它,让它能够支持 TypeScript 文件,这样所有使用者在迁移的时候会更方便。

分号格式化选项

JavaScript 有一个自动分号插入(ASI,automatic semicolon insertion)规则,TypeScript 内置的格式化程序现在能支持在可选的尾分号位置插入或删除分号。该设置现在在 Visual Studio Code Insiders ,以及 Visual Studio 16.4 Preview 2 中的“工具选项”菜单中可用。

New semicolon formatter option in VS Code

将值设定为 “insert” 或 “remove” 同时也会影响自动导入、类型提取、以及其他 TypeScript 服务提供的自动生成代码的格式。将设置保留为默认值 “ignore” 可以使生成代码的分号自动配置匹配当前文件的风格。

3.7 的破坏性变更

DOM 变更

lib.dom.d.ts 中的类型声明已更新。 这些变更大部分是与空值检查有关的检测准确性变更,最终的影响取决于你的代码库。

类字段处理

正如上文提到的,TypeScript 3.7 现在能够在 .d.ts 文件中编译出 get/set,这可能对 3.5 和更低版本的 TypeScript 使用者来说是破坏性变更。 TypeScript 3.6 的使用者不会受影响,因为该版本对这个功能已经进行了预兼容。

useDefineForClassFields 选项虽然自身没有破坏性变更,但不排除以下情形:

  • 在派生类中用属性声明覆盖了基类的访问器
  • 覆盖声明属性,但是没有初始值

要了解全部的影响,请查看 上面关于 useDefineForClassFields 的章节

函数真值检查

正如上文提到的,现在当函数在 if 条件语句中未被调用时 TypeScript 会报错。 当 if 条件语句中判断的是函数时将会报错,除非符合以下情形:

  • 该函数是可选属性
  • 未开启 strictNullChecks 选项
  • 该函数在 if 的代码块中有被调用

本地和导入的类型声明现在会产生冲突

TypeScript 之前有一个 bug,导致允许以下代码结构:

// ./someOtherModule.ts
interface SomeType {
    y: string;
}

// ./myModule.ts
import { SomeType } from "./someOtherModule";
export interface SomeType {
    x: number;
}

function fn(arg: SomeType) {
    console.log(arg.x); // Error! 'x' doesn't exist on 'SomeType'
}

这里,SomeType 同时来源于 import 声明和本地 interface 声明。 出人意料的是,在模块内部,SomeType 只会指向 import 的定义,而本地声明的 SomeType 仅在另一个文件的导入中起效。 这很令人困惑,我们对类似的个例进行的调查表明,广大开发者通常理解的情况不一样。

在 TypeScript 3.7 中,这个问题中的重复声明现在可以被正确地识别为一个错误。 合理的修复方案取决于开发者的原始意图,并应该逐案解决。 通常,命名冲突不是故意的,最好的办法是重命名导入的那个类型。 如果是要扩展导入的类型,则可以编写模块扩展(module augmentation)来代替。

3.7 API 变化

为了实现上文中提到的递归的类型别名模式,TypeReference 接口已经移除了 typeArguments 属性。开发者应该在 TypeChecker 实例上使用 getTypeArguments 函数来代替。

TypeScript 3.6

更严格的生成器

TypeScript 3.6 对迭代器和生成器函数引入了更严格的检查。在之前的版本中,用户无法区分一个值是生成的还是被返回的。

function* foo() {
  if (Math.random() < 0.5) yield 100;
  return "Finished!"
}

let iter = foo();
let curr = iter.next();
if (curr.done) {
  // TypeScript 3.5 以及之前的版本会认为 `value` 为 'string | number'。
  // 当 `done` 为 `true` 的时候,它应该知道 `value` 为 'string'!
  curr.value
}

另外,生成器只假定 yield 的类型为 any

function* bar() {
  let x: { hello(): void } = yield;
  x.hello();
}

let iter = bar();
iter.next();
iter.next(123); // 不好! 运行时错误!

在 TypeScript 3.6 中,在我们第一个例子中检查器现在知道 curr.value 的正确类型应该是 string ,并且,在最后一个例子中当我们调用 next() 时会准确的提示错误。这要感谢在 IteratorIteratorResule 的类型定义包含了一些新的类型参数,并且一个被叫做 Generator 的新类型在 TypeScript 中用来表示生成器。

类型 Iterator 现在允许用户明确的定义生成的类型,返回的类型和 next 能够接收的类型。

interface Iterator<T, TReturn = any, TNext = undefined> {
  // 接受 0 或者 1 个参数 - 不接受 'undefined'
  next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  return?(value?: TReturn): IteratorResult<T, TReturn>;
  throw?(e?: any): IteratorResult<T, TReturn>;
}

以此为基础,新的 Generator 类型是一个迭代器,它总是有 returnthrow 方法,并且也是可迭代的。

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
  next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  return(value: TReturn): IteratorResult<T, TReturn>;
  throw(e: any): IteratorResult<T, TReturn>;
  [Symbol.iterator](): Generator<T, TReturn, TNext>;
}

为了允许在返回值和生成值之间进行区分,TypeScript 3.6 转变 IteratorResult 类型为一个区别对待的联合类型:

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface IteratorYieldResult<TYield> {
  done?: false;
  value: TYield;
}

interface IteratorReturnResult<TReturn> {
  done: true;
  value: TReturn;
}

简而言之,这意味当直接处理迭代器时,你将有能力细化值的类型。

为了正确的表示在调用生成器的 next() 方法的时候能被传入的类型,TypeScript 3.6 还可以在生成器函数内推断出 yield 的某些用法。

function* foo() {
  let x: string = yield;
  console.log(x.toUpperCase());
}

let x = foo();
x.next(); // 第一次调用 `next` 总是被忽略
x.next(42); // 错啦!'number' 和 'string' 不匹配

如果你更喜欢显示的,你还可以使用显示的返回类型强制申明从生成表达式返回的、生成的和计算的的值的类型。下面,next() 只能被 booleans 值调用,并且根据 done 的值,value 可以是 string 或者 number

/**
 * - yields numbers
 * - returns strings
 * - can be passed in booleans
 */
function* counter(): Generator<number, string, boolean> {
  let i = 0;
  while (true) {
    if (yield i++) {
      break;
    }
  }
  return "done!";
}

var iter = counter();
var curr = iter.next()
while (!curr.done) {
  console.log(curr.value);
  curr = iter.next(curr.value === 5)
}
console.log(curr.value.toUpperCase());

// prints:
//
// 0
// 1
// 2
// 3
// 4
// 5
// DONE!

有关更多详细的改变,查看 pull request

更准确的数组展开

在 ES2015 之前的目标中,对于像循环和数组展开之类的结构最忠实的生成可能有点繁重。因此,TypeScript 默认使用更简单的生成,它只支持数组类型,并支持使用 --downlevelIteration 标志迭代其它类型。在此标志下,发出的代码更准确,但更大。

默认情况下 --downlevelIteration 默认关闭效果很好,因为大多数以 ES5 为目标的用户只计划使用带数组的迭代结构。但是,我们支持数组的生成在某些边缘情况下仍然存在一些可观察到的差异。

例如,以下示例:

[...Array(5)]

相当于以下数组:

[undefined, undefined, undefined, undefined, undefined]

但是,TypeScript 会将原始代码转换为此代码:

Array(5).slice();

这略有不同。 Array(5) 生成一个长度为 5 的数组,但并没有在其中插入任何元素!

1 in [undefined, undefined, undefined] // true
1 in Array(3) // false

当 TypeScript 调用 slice() 时,它还会创建一个索引尚未设置的数组。

这可能看起来有点深奥,但事实证明许多用户遇到了这种令人不快的行为。 TypeScript 3.6 不是使用 slice() 和内置函数,而是引入了一个新的 __spreadArrays 辅助程序,以准确地模拟 ECMAScript 2015 中在 --downlevelIteration 之外的旧目标中发生的事情。 __spreadArrays 也可以在 tslib 中使用(如果你正在寻找更小的包,那么值得一试)。

有关更多信息,请参阅相关的 pull request

改进了 Promises 的 UX

Promise 是当今使用异步数据的常用方法之一。不幸的是,使用面向 Promise 的 API 通常会让用户感到困惑。 TypeScript 3.6 引入了一些改进,以防止错误的处理 Promise

例如,在将它传递给另一个函数之前忘记 .then() 或等待 Promise 的完成通常是很常见的。TypeScript 的错误消息现在是专门的,并告知用户他们可能应该考虑使用 await 关键字。

interface User {
  name: string;
  age: number;
  location: string;
}

declare function getUserData(): Promise<User>;
declare function displayUser(user: User): void;

async function f() {
  displayUser(getUserData());
//            ~~~~~~~~~~~~~
// 'Promise <User>' 类型的参数不能分配给 'User' 类型的参数。
//   ...
// 你忘记使用 'await' 吗?
}

在等待或 .then() - Promise 之前尝试访问方法也很常见。这是另一个例子,在许多其他方面,我们能够做得更好。

async function getCuteAnimals() {
  fetch("https://reddit.com/r/aww.json")
    .json()
  // ~~~~
  // 'Promise <Response>'类型中不存在属性'json'。
  // 你忘记使用'await'吗?
}

目的是即使用户不知道需要等待,至少,这些消息提供了更多关于从何处开始的上下文。

与可发现性相同,让您的生活更轻松 - 除了 Promises 上更好的错误消息之外,我们现在还在某些情况下提供快速修复。

正在应用快速修复以添加缺少的 `await` 关键字。

有关更多详细信息,请参阅原始问题以及链接回来的 pull request

标识符更好的支持 Unicode

当发射到 ES2015 及更高版本的目标时,TypeScript 3.6 在标识符中包含对 Unicode 字符的更好支持。

const 𝓱𝓮𝓵𝓵𝓸 = "world"; // previously disallowed, now allowed in '--target es2015'
// 以前不允许,现在在 '--target es2015' 中允许

支持在 SystemJS 中使用 import.meta

当模块目标设置为 system 时,TypeScript 3.6 支持将 import.meta 转换为 context.meta

// 此模块:
console.log(import.meta.url)

// 获得如下的转变:
System.register([], function (exports, context) {
  return {
    setters: [],
    execute: function () {
      console.log(context.meta.url);
    }
  };
});

在环境上下文中允许 getset 访问者

在以前的 TypeScript 版本中,该语言不允许在环境上下文中使用 getset 访问器(例如,在 declare-d 类中,或者在 .d.ts 文件中)。理由是,就这些属性的写作和阅读而言,访问者与属性没有区别,但是,因为 ECMAScript 的类字段提议可能与现有版本的 TypeScript 具有不同的行为,我们意识到我们需要一种方法来传达这种不同的行为,以便在子类中提供适当的错误。

因此,用户可以在 TypeScript 3.6 中的环境上下文中编写 gettersetter

declare class Foo {
  // 3.6+ 允许
  get x(): number;
  set x(val: number): void;
}

在TypeScript 3.7中,编译器本身将利用此功能,以便生成的 .d.ts 文件也将生成 get / set 访问器。

环境类和函数可以合并

在以前版本的 TypeScript 中,在任何情况下合并类和函数都是错误的。现在,环境类和函数(具有 declare 修饰符的类/函数或 .d.ts 文件中)可以合并。这意味着现在您可以编写以下内容:

export declare function Point2D(x: number, y: number): Point2D;
export declare class Point2D {
  x: number;
  y: number;
  constructor(x: number, y: number);
}

而不需要使用

export interface Point2D {
    x: number;
    y: number;
}
export declare var Point2D: {
    (x: number, y: number): Point2D;
    new (x: number, y: number): Point2D;
}

这样做的一个优点是可以很容易地表达可调用的构造函数模式,同时还允许名称空间与这些声明合并(因为 var 声明不能与名称空间合并)。

在 TypeScript 3.7 中,编译器将利用此功能,以便从 .js 文件生成的 .d.ts 文件可以适当地捕获类类函数的可调用性和可构造性。

有关更多详细信息,请参阅 GitHub 上的原始 PR

APIs 支持 --build--incremental

TypeScript 3.0 引入了对引用其他项目的支持,并使用 --build 标志以增量方式构建它们。此外,TypeScript 3.4 引入了 --incremental 标志,用于保存有关以前编译的信息,仅重建某些文件。这些标志对于更灵活地构建项目和加速构建非常有用。不幸的是,使用这些标志不适用于 Gulp 和 Webpack 等第三方构建工具。TypeScript 3.6 现在公开了两组 API 来操作项目引用和增量构建。

对于创建 --incremental 构建,用户可以利用 createIncrementalProgramcreateIncrementalCompilerHost API。用户还可以使用新公开的 readBuilderProgram 函数从此 API 生成的 .tsbuildinfo 文件中重新保存旧程序实例,该函数仅用于创建新程序(即,您无法修改返回的实例 - 它意味着用于其他 create * Program 函数中的 oldProgram 参数)。

为了利用项目引用,公开了一个新的 createSolutionBuilder 函数,它返回一个新类型 SolutionBuilder 的实例。

有关这些 API 的更多详细信息,您可以查看原始 pull request

新的 TypeScript Playground

TypeScript Playground 已经获得了急需的刷新功能,并提供了便利的新功能!Playground 主要是 Artem TyurinTypeScript Playground 的一个分支,社区成员越来越多地使用它。我们非常感谢 Artem 在这里提供帮助!

新的 Playground 现在支持许多新的选项,包括:

  • target 选项(允许用户切换输出 es5es3es2015esnext 等)
  • 所有的严格检查标记(包括 just strict
  • 支持纯 JavaScript 文件(使用 allowJs 和可选的 checkJs

当分享 Playground 的链接时,这些选项也会保存下来,允许用户更可靠地分享示例,而无需告诉受众“哦,别忘了打开 noImplicitAny 选项!”。

在不久的将来,我们将更新 Playground 样本,添加 JSX 支持和改进自动类型获取,这意味着您将能够在 Playground 上体验到与编辑器中相同的体验。

随着我们改进 Playground 和网站,我们欢迎GitHub上的issue 和 pull request

代码编辑的分号感知

对于 Visual Studio 和 Visual Studio Code 编辑器可以自动的应用快速修复、重构和自动从其它模块导入值等其它的转换。这些转换都由 TypeScript 来驱动,老版本的 TypeScript 无条件的在语句的末尾添加分号,不幸的是,这和大多数用户的代码风格不相符,并且,很多用户对于编辑器自动输入分号很不爽。

TypeScript 现在在应用这些简短的编辑的时候,已经足够的智能去检测你的文件分号的使用情况。如果你的文件通常缺少分号,TypeScript 就不会添加分号。

更多细节,查看这些 pull request

更智能的自动导入

JavaScript 有大量不同的模块语法或者约定:EMACScript standard、CommonJS、AMD、System.js 等等。在大多数的情况下,TypeScript 默认使用 ECMAScript standard 语法自动导入,这在具有不同编译器设置的某些 TypeScript 项目中通常是不合适的,或者在使用纯 JavaScript 和需要调用的 Node 项目中。

在决定如何自动导入模块之前,TypeScript 3.6 现在会更加智能的查看你的现有导入。你可以通过这些 pull request查看更多细节。

接下来?

要了解团队将要开展的工作,请查看今年 7 月至 12 月的 6 个月路线图

与往常一样,我们希望这个版本的 TypeScript 能让编码体验更好,让您更快乐。如果您有任何建议或遇到任何问题,我们总是感兴趣,所以随时在GitHub上提一个 issue

参考

TypeScript 3.5

改进速度

TypeScript 3.5 为类型检查和增量构建采用了几个优化。

类型检查速度提升

TypeScript 3.5 包含对 TypeScript 3.4 的某些优化,可以更高效地进行类型检查。 在代码补全列表等类型检查驱动的操作上,这些改进效果显著。

改进 --incremental

TypeScript 3.5 通过缓存计算状态的信息(编译器设置、寻找文件的原因、文件在哪里被找到等等),改进了在 3.4 中的 --incremental 构建模式。我们发现重新构建花费的时间比 TypeScript 3.4 减少了 68%!

有关更多信息,你可以查看这些 pull requests

Omit 辅助类型

TypeScript 3.5 添加了新的 Omit 辅助类型,这个类型用来创建从原始类型中移除了某些属性的新类型。

type Person = {
  name: string;
  age: number;
  location: string;
};

type QuantumPerson = Omit<Person, "location">;

// 相当于
type QuantumPerson = {
  name: string;
  age: number;
};

使用 Omit 辅助,我们有能力复制 Person 中除了 location 之外的所有属性。

有关更多细节,在 GitHub 查看添加 Omit 的 pull request, 以及有关剩余对象使用 Omit 的更改

改进了联合类型中多余属性的检查

在 TypeScript 3.4 及之前的版本中,会出现确实不应该存在的多余属性却被允许存在的情况。 例如,TypeScript 3.4 在对象字面量上允许不正确的 name 属性,甚至它的类型在 PointLabel 之中都不匹配。

type Point = {
  x: number;
  y: number;
};

type Label = {
  name: string;
};

const thing: Point | Label = {
  x: 0,
  y: 0,
  name: true // uh-oh!
};

以前,一个无区别的联合在它的成员上不会进行_任何_多余属性的检查,结果,类型错误的 name 属性溜了进来。

在 TypeScript 3.5 中,类型检查器至少会验证所有提供的属性属于_某个_联合类型的成员,且类型恰当,这意味着,上面的例子会正确的进行错误提示。

注意,只要属性类型有效,仍允许部分重叠。

const pl: Point | Label = {
  x: 0,
  y: 0,
  name: "origin" // okay
};

--allowUmdGlobalAccess 标志

在 TypeScript 3.5 中,使用新的 --allowUmdGlobalAccess 标志,你现在可以从任何位置引用全局的 UMD 申明——甚至模块。

export as namespace foo;

此模式增加了混合和匹配第三方库的灵活性,其中库声明的全局变量总是可以被使用,甚至可以从模块内部使用。

有关更多细节,查看 GitHub 上的 pull request

更智能的联合类型检查

在 TypeScript 3.4 以及之前的版本中,下面的例子会无效:

type S = { done: boolean, value: number }
type T =
  | { done: false, value: number }
  | { done: true, value: number };

declare let source: S;
declare let target: T;

target = source;

这是因为 S 无法被分配给 { done: false, value: number } 或者 { done: true, value: number }。 为啥? 因为属性 doneS 不够具体——他是 boolean。而 T 的的每个成员有一个明确的为 true 或者 false 属性 done

这就是我们单独检查每个成员的意义:TypeScript 不只是将每个属性合并在一起,看看是否可以赋予 S

如果这样做,一些糟糕的代码可能会像下面这样:

interface Foo {
  kind: "foo";
  value: string;
}

interface Bar {
  kind: "bar";
  value: number;
}

function doSomething(x: Foo | Bar) {
  if (x.kind === "foo") {
    x.value.toLowerCase();
  }
}

// uh-oh - 幸运的是, TypeScript 在这里会提示错误!
doSomething({
  kind: "foo",
  value: 123,
});

然而,对于原始的例子,这有点过于严格。 如果你弄清除 S 的任何可能值的精确类型,你实际上可以看到它与 T 中的类型完全匹配。

在 TypeScript 3.5 中,当分配具有辨别属性的类型时,如 T,实际上_将_进一步将类似 S 的类型分解为每个可能的成员类型的并集。 在这种情况下,由于 booleantruefalse 的联合,S 将被视为 {done:false,value:number}{done:true,value:number }

有关更多细节,你可以在 GitHub 上查看原始的 pull request

泛型构造函数的高阶类型推断

在 TypeScript 3.4 中,我们改进了对返回函数的泛型函数的推断:

function compose<T, U, V>(f: (x: T) => U, g: (y: U) => V): (x: T) => V {
  return x => g(f(x))
}

将其他泛型函数作为参数,如下所示:

function arrayify<T>(x: T): T[] {
  return [x];
}

type Box<U> = { value: U }
function boxify<U>(y: U): Box<U> {
  return { value: y };
}

let newFn = compose(arrayify, boxify);

TypeScript 3.4 的推断允许 newFn 是泛型的。它的新类型是 <T>(x:T)=> Box <T []>。而不是旧版本推断的,相对无用的类型,如 (x:{})=> Box <{} []>

TypeScript 3.5 在处理构造函数的时候推广了这种行为。

class Box<T> {
  kind: "box";
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}

class Bag<U> {
  kind: "bag";
  value: U;
  constructor(value: U) {
    this.value = value;
  }
}

function composeCtor<T, U, V>(F: new (x: T) => U, G: new (y: U) => V): (x: T) => V {
  return x => new G(new F(x))
}

let f = composeCtor(Box, Bag); // 拥有类型 '<T>(x: T) => Bag<Box<T>>'
let a = f(1024); // 拥有类型 'Bag<Box<number>>'

除了上面的组合模式之外,这种对泛型构造函数的新推断意味着在某些 UI 库(如 React )中对类组件进行操作的函数可以更正确地对泛型类组件进行操作。

type ComponentClass<P> = new (props: P) => Component<P>;
declare class Component<P> {
  props: P;
  constructor(props: P);
}

declare function myHoc<P>(C: ComponentClass<P>): ComponentClass<P>;

type NestedProps<T> = { foo: number, stuff: T };

declare class GenericComponent<T> extends Component<NestedProps<T>> { }

// 类型为 'new <T>(props: NestedProps<T>) => Component<NestedProps<T>>'
const GenericComponent2 = myHoc(GenericComponent);

想学习更多,在 GitHub 上查看原始的 pull requet

参考

TypeScript 3.4

使用 --incremental 标志加快后续构建

TypeScript 3.4 引入了一个名为 --incremental 的新标志,它告诉 TypeScript 从上一次编译中保存有关项目图的信息。

下次使用 --incremental 调用 TypeScript 时,它将使用该信息来检测类型检查和生成对项目更改成本最低的方法。

// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "outDir": "./lib"
  },
  "include": ["./src"]
}

默认使用这些设置,当我们运行 tsc 时,TypeScript 将在输出目录(./lib)中查找名为 .tsbuildinfo 的文件。 如果 ./lib/.tsbuildinfo 不存在,它将被生成。 但如果存在,tsc 将尝试使用该文件逐步进行类型检查并更新输出文件。

这些 .tsbuildinfo 文件可以安全地删除,并且在运行时对我们的代码没有任何影响——它们纯粹用于更快地编译。 我们也可以将它们命名为我们想要的任何名字,并使用 --tsBuildInfoFile 标志将它们放在我们想要的任何位置。

// front-end.tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./buildcache/front-end",
    "outDir": "./lib"
  },
  "include": ["./src"]
}

复合项目

复合项目的意图的一部分(tsconfig.jsons,composite 设置为 true)是不同项目之间的引用可以增量构建。 因此,复合项目将始终生成 .tsbuildinfo 文件。

outFile

当使用 outFile 时,构建信息文件的名称将基于输出文件的名称。 例如,如果我们的输出 JavaScript 文件是 ./ output / foo.js,那么在 --incremental 标志下,TypeScript 将生成文件./output/foo.tsbuildinfo。 如上所述,这可以通过 --tsBuildInfoFile 标志来控制。

泛型函数的高阶类型推断

当来自其它泛型函数的推断产生用于推断的自由类型变量时,TypeScript 3.4 现在可以生成泛型函数类型。

这意味着在 3.4 中许多函数组合模式现在运行的更好了。

为了更具体,让我们建立一些动机并考虑以下 compose 函数:

function compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
  return x => g(f(x));
}

compose 还有两个其他函数:

  • f 它接受一些参数(类型为 A)并返回类型为 B 的值
  • g 采用类型为 B 的参数(类型为 f 返回),并返回类型为 C 的值

compose 然后返回一个函数,它通过 f 然后 g 来提供它的参数。

调用此函数时,TypeScript 将尝试通过一个名为 type argument inference 的进程来计算出 ABC 的类型。 这个推断过程通常很有效:

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

function getDisplayName(p: Person) {
  return p.name.toLowerCase();
}

function getLength(s: string) {
  return s.length;
}

// 拥有类型 '(p: Person) => number'
const getDisplayNameLength = compose(
  getDisplayName,
  getLength,
);

// 有效并返回 `number` 类型
getDisplayNameLength({ name: "Person McPersonface", age: 42 });

推断过程在这里相当简单,因为 getDisplayNamegetLength 使用的是可以轻松引用的类型。 但是,在 TypeScript 3.3 及更早版本中,泛型函数如 compose 在传递其他泛型函数时效果不佳。

interface Box<T> {
  value: T;
}

function makeArray<T>(x: T): T[] {
  return [x];
}

function makeBox<U>(value: U): Box<U> {
  return { value };
}

// 类型为 '(arg: {}) => Box<{}[]>'
const makeBoxedArray = compose(
  makeArray,
  makeBox,
)

makeBoxedArray("hello!").value[0].toUpperCase();
//                                ~~~~~~~~~~~
// 错误:类型 '{}' 没有 'toUpperCase' 属性

在旧版本中,当从其他类型变量(如 TU)推断时,TypeScript 会推断出空对象类型({})。

在 TypeScript 3.4 中的类型参数推断时,对于返回函数的泛型函数的调用,TypeScript (视情况而定)把类型参数从泛型函数参数传递到生成的函数类型中。

换句话说,而不是生成类型

(arg: {}) => Box<{}[]>

TypeScript 3.4 生成的类型

<T>(arg: T) => Box<T[]>

注意,T 已从 makeArray 传递到结果类型的类型参数列表中。 这意味着来自 compose 参数的泛型已被保留,我们的 makeBoxedArray 示例将正常运行!

interface Box<T> {
  value: T;
}

function makeArray<T>(x: T): T[] {
  return [x];
}

function makeBox<U>(value: U): Box<U> {
  return { value };
}

// 类型为 '<T>(arg: T) => Box<T[]>'
const makeBoxedArray = compose(
  makeArray,
  makeBox,
)

// 正常运行!
makeBoxedArray("hello!").value[0].toUpperCase();

更多细节,你可以读到更多从这些原始的变动

改进 ReadonlyArrayreadonly 元祖

TypeScript 3.4 让使用只读的类似数组的类型更简单了。

一个与 ReadonlyArray 相关的新语法

ReadonlyArray 类型描述 Array 是只读的。

任何带有 ReadonlyArray 引用的变量不能被添加、移除或者替换数组中的任何元素。

function foo(arr: ReadonlyArray<string>) {
  arr.slice();        // okay
  arr.push("hello!"); // error!
}

当期待数组不可变时使用 ReadonlyArray 替代 Array 是好实践,考虑到数组有一个更棒的语法的情况下这通常有一点痛苦。 尤其是,number[] 是一个省略版的 Array<number>,就像 Date[] 是省略版的 Array<Date>

TypeScript 3.4 为 ReadonlyArray 引入了一个新的语法,就是在数组类型上使用了新的 readonly 修饰语。

function foo(arr: readonly string[]) {
  arr.slice();        // okay
  arr.push("hello!"); // 错误!
}

readonly 元祖

TypeScript 3.4 同样引入了对 readonly 元祖的支持。 我们可以在任何元祖类型上加上前置 readonly 关键字用来表示它是 readonly 元祖,非常像我们现在可以对数组使用的省略版语法。 就像你可能期待的,不像插槽可写的普通元祖,readonly 元祖只允许从那些位置读。

function foo(pair: readonly [string, string]) {
  console.log(pair[0]);   // okay
  pair[1] = "hello!";     // 错误
}

普通的元祖是用相同的方式从 Array 继承的——一个元祖T1, T2, ... Tn 继承自 Array< T1 | T2 | ... Tn > - readonly 元祖是继承自类型 ReadonlyArray。所以,一个 readonly 元祖 T1, T2, ... Tn 继承自 ReadonlyArray< T1 | T2 | ... Tn >

映射类型修饰语 readonlyreadonly 数组

在之前的 TypeScript 版本中,我们一般使用映射类型操作不同的类似数组的结构。

这意味着,一个映射类型像 Boxify 可以在数组上生效,元祖也是。

interface Box<T> { value: T }

type Boxify<T> = {
}

// { a: Box<string>, b: Box<number> }
type A = Boxify<{ a: string, b: number }>;

// Array<Box<number>>
type B = Boxify<number[]>;

// [Box<string>, Box<number>]
type C = Boxify<[string, boolean]>;

不幸的是,映射类型像 Readonly 实用类型在数组和元祖类型上实际上是无用的。

// lib.d.ts
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 在 TypeScript 3.4 之前代码会如何执行

// { readonly a: string, readonly b: number }
type A = Readonly<{ a: string, b: number }>;

// number[]
type B = Readonly<number[]>;

// [string, boolean]
type C = Readonly<[string, boolean]>;

在 TypeScript 3.4,在映射类型中的 readonly 修饰符将自动的转换类似数组结构到他们相符合的 readonly 副本。

// 在 TypeScript 3.4 中代码会如何运行

// { readonly a: string, readonly b: number }
type A = Readonly<{ a: string, b: number }>;

// readonly number[]
type B = Readonly<number[]>;

// readonly [string, boolean]
type C = Readonly<[string, boolean]>;

类似地,你可以编写一个类似 Writable 映射类型的实用程序类型来移除 readonly-ness,并将 readonly 数组容器转换回它们的可变等价物。

type Writable<T> = {
  -readonly [K in keyof T]: T[K]
}

// { a: string, b: number }
type A = Writable<{
  readonly a: string;
  readonly b: number
}>;

// number[]
type B = Writable<readonly number[]>;

// [string, boolean]
type C = Writable<readonly [string, boolean]>;

注意事项

它不是一个通用型操作,尽管它看起来像。 readonly 类型修饰符只能用于数组类型和元组类型的语法。

let err1: readonly Set<number>; // 错误!
let err2: readonly Array<boolean>; // 错误!

let okay: readonly boolean[]; // 有效

你可以查看 pull request 了解更多详情

const 断言

TypeScript 3.4 引入了一个叫 const 断言的字面量值的新构造。 它的语法是用 const 代替类型名称的类型断言(例如 123 as const)。 当我们用 const 断言构造新的字面量表达式时,我们可以用来表示:

  • 该表达式中的字面量类型不应粗化(例如,不要从 'hello'string
  • 对象字面量获得 readonly 属性
  • 数组字面量成为 readonly 元组
// Type '"hello"'
let x = "hello" as const;

// Type 'readonly [10, 20]'
let y = [10, 20] as const;

// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;

也可以使用尖括号断言语法,除了 .tsx 文件之外。

// Type '"hello"'
let x = <const>"hello";

// Type 'readonly [10, 20]'
let y = <const>[10, 20];

// Type '{ readonly text: "hello" }'
let z = <const>{ text: "hello" };

此功能意味着通常可以省略掉仅用于将不可变性示意给编译器的类型。

// 不使用引用或声明的类型。
// 我们只需要一个 const 断言。
function getShapes() {
  let result = [
    { kind: "circle", radius: 100, },
    { kind: "square", sideLength: 50, },
  ] as const;

  return result;
}

for (const shape of getShapes()) {
  // 完美细化
  if (shape.kind === "circle") {
    console.log("Circle radius", shape.radius);
  }
  else {
    console.log("Square side length", shape.sideLength);
  }
}

请注意,上面的例子不需要类型注释。 const 断言允许 TypeScript 采用最具体的类型表达式。

如果你选择不使用 TypeScript 的 enum 结构,这甚至可以用于在纯 JavaScript 代码中使用类似 enum 的模式。

export const Colors = {
  red: "RED",
  blue: "BLUE",
  green: "GREEN",
} as const;

// 或者使用 'export default'

export default {
  red: "RED",
  blue: "BLUE",
  green: "GREEN",
} as const;

注意事项

需要注意的是,const 断言只能直接应用于简单的字面量表达式上。

// 错误!'const' 断言只能用在 string, number, boolean, array, object literal。
let a = (Math.random() < 0.5 ? 0 : 1) as const;

// 有效!
let b = Math.random() < 0.5 ?
  0 as const :
  1 as const;

另一件得记住的事是 const 上下文不会直接将表达式转换为完全不可变的。

let arr = [1, 2, 3, 4];

let foo = {
  name: "foo",
  contents: arr,
} as const;

foo.name = "bar";   // 错误!
foo.contents = [];  // 错误!

foo.contents.push(5); // ...有效!

更多详情,你可以查看相应的 pull request

globalThis 的类型检查

TypeScript 3.4 引入了对 ECMAScript 新 globalThis 全局变量的类型检查的支持,它指向的是全局作用域。 与上述解决方案不同,globalThis 提供了一种访问全局作用域的标准方法,可以在不同环境中使用。

// 在一个全局文件里:

var abc = 100;

// 指向上面的 `abc`
globalThis.abc = 200;

注意,使用 letconst 声明的全局变量不会显示在 globalThis 上。

let answer = 42;

// 错误!'typeof globalThis' 没有 'answer' 属性。
globalThis.answer = 333333;

同样重要的是要注意,在编译为老版本的 ECMAScript 时,TypeScript 不会转换引用到 globalThis 上。 因此,除非您的目标是常青浏览器(已经支持 globalThis),否则您可能需要使用 polyfill

更多详细信息,请参阅该功能的 pull request

参考

TypeScript 3.3

改进调用联合类型时的行为

在 TypeScript 之前的版本中,将可调用类型联合后仅在它们具有相同的参数列表时才能被调用。

type Fruit = "apple" | "orange";
type Color = "red" | "orange";

type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors

declare let f: FruitEater | ColorConsumer;

// Cannot invoke an expression whose type lacks a call signature.
//   Type 'FruitEater | ColorConsumer' has no compatible call signatures.ts(2349)
f("orange");

然而,上例中,FruitEaterColorConsumer应该都可以使用"orange",并返回numberstring

在 TypeScript 3.3 里,这个错误不存在了。

type Fruit = "apple" | "orange";
type Color = "red" | "orange";

type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors

declare let f: FruitEater | ColorConsumer;

f("orange"); // It works! Returns a 'number | string'.

f("apple"); // error - Argument of type '"apple"' is not assignable to parameter of type '"orange"'.

f("red"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.

TypeScript 3.3,这些签名的参数被连结在一起构成了一个新的签名。

在上例中,fruitcolor连结在一起形成新的参数类型Fruit & ColorFruit & Color("apple" | "orange") & ("red" | "orange")是一样的,都相当于("apple" & "red") | ("apple" & "orange") | ("orange" & "red") | ("orange" & "orange")。 那些不可能交叉的会规约成never类型,只剩下"orange" & "orange",就是"orange"

警告

这个新行为仅在满足如下情形时生效:

  • 联合类型中最多有一个类型具有多个重载,
  • 联合类型中最多有一个类型有泛型签名。

这意味着,像map这种操作number[] | string[]的方法,还是不能调用,因为map是泛型函数。

另一方面,像forEach就可以调用,因为它不是泛型函数,但在noImplicitAny模式可能有些问题。

interface Dog {
  kind: "dog";
  dogProp: any;
}
interface Cat {
  kind: "cat";
  catProp: any;
}

const catOrDogArray: Dog[] | Cat[] = [];

catOrDogArray.forEach(animal => {
  //                ~~~~~~ error!
  // Parameter 'animal' implicitly has an 'any' type.
});

添加显式的类型信息可以解决。

interface Dog {
  kind: "dog";
  dogProp: any;
}
interface Cat {
  kind: "cat";
  catProp: any;
}

const catOrDogArray: Dog[] | Cat[] = [];
catOrDogArray.forEach((animal: Dog | Cat) => {
  if (animal.kind === "dog") {
    animal.dogProp;
    // ...
  } else if (animal.kind === "cat") {
    animal.catProp;
    // ...
  }
});

在合复合工程中增量地检测文件的变化 --build --watch

TypeScript 3.0 引入了一个新特性来按结构进行构建,称做“复合工程”。 目的是让用户能够把大型工程拆分成小的部分从而快速构建并保留项目结构。 正是因为支持了复合工程,TypeScript 可以使用--build模式仅重新编译部分工程和依赖。 可以把它当做工作内部构建的一种优化。

TypeScript 2.7 还引入了--watch构建模式,它使用了新的增量"builder"API。 背后的想法都是仅重新检查和生成改动过的文件或者是依赖项可能影响类型检查的文件。 可以把它们当成工程内部构建的优化。

在 3.3 之前,使用--build --watch构建复合工程不会真正地使用增量文件检测机制。 在--build --watch模式下,一个工程里的一处改动会导致整个工程重新构建,而非仅检查那些真正受到影响的文件。

在 TypeScript 3.3 里,--build模式的--watch标记也会使用增量文件检测。 因此--build --watch模式下构建非常快。 我们的测试结果显示,这个功能会减少 50%到 75%的构建时间,相比于原先的--build --watch。 具体数字在这这个pull request里,我们相信大多数复合工程用户会看到明显效果。

TypeScript 3.2

strictBindCallApply

TypeScript 3.2引入了一个新的--strictBindCallApply编译选项(是--strict选项家族之一)。在使用了此选项后,函数对象上的bindcallapply方法将应用强类型并进行严格的类型检查。

function foo(a: number, b: string): string {
    return a + b;
}

let a = foo.apply(undefined, [10]);              // error: too few argumnts
let b = foo.apply(undefined, [10, 20]);          // error: 2nd argument is a number
let c = foo.apply(undefined, [10, "hello", 30]); // error: too many arguments
let d = foo.apply(undefined, [10, "hello"]);     // okay! returns a string

它的实现是通过引入了两种新类型来完成的,即lib.d.ts里的CallableFunctionNewableFunction。这些类型包含了针对常规函数和构造函数上bindcallapply的泛型方法声明。这些声明使用了泛型剩余参数来捕获和反射参数列表,使之具有强类型。在--strictBindCallApply模式下,这些声明作用在Function类型声明出现的位置。

警告

由于更严格的检查可能暴露之前没发现的错误,因此这是--strict模式下的一个破坏性改动。

此外,这个新功能还有另一个警告。由于有这些限制,bindcallapply无法为重载的泛型函数或重载的函数进行完整地建模。 当在泛型函数上使用这些方法时,类型参数会被替换为空对象类型({}),并且若在有重载的函数上使用这些方法时,只有最后一个重载会被建模。

对象字面量的泛型展开表达式

TypeScript 3.2开始,对象字面量允许泛型展开表达式,它产生交叉类型,和Object.assign函数或JSX字面量类似。例如:

function taggedObject<T, U extends string>(obj: T, tag: U) {
    return { ...obj, tag };  // T & { tag: U }
}

let x = taggedObject({ x: 10, y: 20 }, "point");  // { x: number, y: number } & { tag: "point" }

属性赋值和非泛型展开表达式会最大程度地合并到泛型展开表达式的一侧。例如:

function foo1<T>(t: T, obj1: { a: string }, obj2: { b: string }) {
    return { ...obj1, x: 1, ...t, ...obj2, y: 2 };  // { a: string, x: number } & T & { b: string, y: number }
}

非泛型展开表达式与之前的行为相同:函数调用签名和构造签名被移除,仅有非方法的属性被保留,针对同名属性则只有出现在最右侧的会被使用。它与交叉类型不同,交叉类型会连接调用签名和构造签名,保留所有的属性,合并同名属性的类型。因此,当展开使用泛型初始化的相同类型时可能会产生不同的结果:

function spread<T, U>(t: T, u: U) {
    return { ...t, ...u };  // T & U
}

declare let x: { a: string, b: number };
declare let y: { b: string, c: boolean };

let s1 = { ...x, ...y };  // { a: string, b: string, c: boolean }
let s2 = spread(x, y);    // { a: string, b: number } & { b: string, c: boolean }
let b1 = s1.b;  // string
let b2 = s2.b;  // number & string

泛型对象剩余变量和参数

TypeScript 3.2开始允许从泛型变量中解构剩余绑定。它是通过使用lib.d.ts里预定义的PickExclude助手类型,并结合使用泛型类型和解构式里的其它绑定名实现的。

function excludeTag<T extends { tag: string }>(obj: T) {
    let { tag, ...rest } = obj;
    return rest;  // Pick<T, Exclude<keyof T, "tag">>
}

const taggedPoint = { x: 10, y: 20, tag: "point" };
const point = excludeTag(taggedPoint);  // { x: number, y: number }

BigInt

BigInt里ECMAScript的一项提案,它在理论上允许我们建模任意大小的整数。 TypeScript 3.2可以为BigInit进行类型检查,并支持在目标为esnext时输出BigInit字面量。

为支持BigInt,TypeScript引入了一个新的原始类型bigint(全小写)。 可以通过调用BigInt()函数或书写BigInt字面量(在整型数字字面量末尾添加n)来获取bigint

let foo: bigint = BigInt(100); // the BigInt function
let bar: bigint = 100n;        // a BigInt literal

// *Slaps roof of fibonacci function*
// This bad boy returns ints that can get *so* big!
function fibonacci(n: bigint) {
    let result = 1n;
    for (let last = 0n, i = 0n; i < n; i++) {
        const current = result;
        result += last;
        last = current;
    }
    return result;
}

fibonacci(10000n)

尽管你可能会认为numberbigint能互换使用,但它们是不同的东西。

declare let foo: number;
declare let bar: bigint;

foo = bar; // error: Type 'bigint' is not assignable to type 'number'.
bar = foo; // error: Type 'number' is not assignable to type 'bigint'.

ECMAScript里规定,在算术运算符里混合使用numberbigint是一个错误。 应该显式地将值转换为BigInt

console.log(3.141592 * 10000n);     // error
console.log(3145 * 10n);            // error
console.log(BigInt(3145) * 10n);    // okay!

还有一点要注意的是,对bigint使用typeof操作符返回一个新的字符串:"bigint"。 因此,TypeScript能够正确地使用typeof细化类型。

function whatKindOfNumberIsIt(x: number | bigint) {
    if (typeof x === "bigint") {
        console.log("'x' is a bigint!");
    }
    else {
        console.log("'x' is a floating-point number");
    }
}

感谢Caleb Sander为实现此功能的付出。

警告

BigInt仅在目标为esnext时才支持。 可能不是很明显的一点是,因为BigInts针对算术运算符+, -, *等具有不同的行为,为老旧版(如es2017及以下)提供此功能时意味着重写出现它们的每一个操作。 TypeScript需根据类型和涉及到的每一处加法,字符串拼接,乘法等产生正确的行为。

因为这个原因,我们不会立即提供向下的支持。 好的一面是,Node 11和较新版本的Chrome已经支持了这个特性,因此你可以在目标为esnext时,使用BigInt。

一些目标可能包含polyfill或类似BigInt的运行时对象。 基于这些考虑,你可能会想要添加esnext.bigintlib编译选项里。

Non-unit types as union discriminants

TypeScript 3.2放宽了作为判别式属性的限制,来让类型细化变得容易。 如果联合类型的共同属性包含了_某些_单体类型(如,字面符字面量,nullundefined)且不包含泛型,那么它就可以做为判别式。

因此,TypeScript 3.2认为下例中的error属性可以做为判别式。这在之前是不可以的,因为Error并非是一个单体类型。 那么,unwrap函数体里的类型细化就可以正确地工作了。

type Result<T> =
    | { error: Error; data: null }
    | { error: null; data: T };

function unwrap<T>(result: Result<T>) {
    if (result.error) {
        // Here 'error' is non-null
        throw result.error;
    }

    // Now 'data' is non-null
    return result.data;
}

tsconfig.json可以通过Node.js包来继承

TypeScript 3.2现在可以从node_modules里解析tsconfig.json。如果tsconfig.json文件里的"extends"设置为空,那么TypeScript会检测node_modules包。 When using a bare path for the "extends" field in tsconfig.json, TypeScript will dive into node_modules packages for us.

{
    "extends": "@my-team/tsconfig-base",
    "include": ["./**/*"]
    "compilerOptions": {
        // Override certain options on a project-by-project basis.
        "strictBindCallApply": false,
    }
}

这里,TypeScript会去node_modules目录里查找@my-team/tsconfig-base包。针对每一个包,TypeScript检查package.json里是否包含"tsconfig"字段,如果是,TypeScript会尝试从那里加载配置文件。如果两者都不存在,TypeScript尝试从根目录读取tsconfig.json。这与Nodejs查找.js文件或TypeScript查找.d.ts文件的已有过程类似。

这个特性对于大型组织或具有很多分布的依赖的工程特别有帮助。

The new --showConfig flag

tsc,TypeScript编译器,支持一个新的标记--showConfig。 运行tsc --showConfig时,TypeScript计算生效的tsconfig.json并打印(继承的配置也会计算在内)。 这对于调试诊断配置问题很有帮助。

JavaScript的Object.defineProperty声明

在编写JavaScript文件时(使用allowJs),TypeScript能识别出使用Object.defineProperty声明。 也就是说会有更好的代码补全功能,和强类型检查,这需要在JavaScript文件里启用类型检查功能(打开checkJs选项或在文件顶端添加// @ts-check注释)。

// @ts-check

let obj = {};
Object.defineProperty(obj, "x", { value: "hello", writable: false });

obj.x.toLowercase();
//    ~~~~~~~~~~~
//    error:
//     Property 'toLowercase' does not exist on type 'string'.
//     Did you mean 'toLowerCase'?

obj.x = "world";
//  ~
//  error:
//   Cannot assign to 'x' because it is a read-only property.

TypeScript 3.1

元组和数组上的映射类型

TypeScript 3.1,在元组和数组上的映射对象类型现在会生成新的元组/数组,而非创建一个新的类型并且这个类型上具有如push()pop()length这样的成员。 例子:

type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };

type Coordinate = [number, number]

type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]

MapToPromise接收参数T,当它是个像Coordinate这样的元组时,只有数值型属性会被转换。 [number, number]具有两个数值型属性:01。 针对这样的数组,MapToPromise会创建一个新的元组,01属性是原类型的一个Promise。 因此PromiseCoordinate的类型为[Promise<number>, Promise<number>]

函数上的属性声明

TypeScript 3.1提供了在函数声明上定义属性的能力,还支持const声明的函数。只需要在函数直接给属性赋值就可以了。 这样我们就可以规范JavaScript代码,不必再借助于namespace。 例子:

function readImage(path: string, callback: (err: any, image: Image) => void) {
    // ...
}

readImage.sync = (path: string) => {
    const contents = fs.readFileSync(path);
    return decodeImageSync(contents);
}

这里,readImage函数异步地读取一张图片。 此外,我们还在readImage上提供了一个便捷的函数readImage.sync

一般来说,使用ECMAScript导出是个更好的方式,但这个新功能支持此风格的代码能够在TypeScript里执行。 此外,这种属性声明的方式允许我们表达一些常见的模式,例如React函数组件(之前叫做SFC)里的defaultPropspropTpes

export const FooComponent = ({ name }) => (
    <div>Hello! I am {name}</div>
);

FooComponent.defaultProps = {
    name: "(anonymous)",
};

[1] 更确切地说,是上面那种同态映射类型。

使用typesVersions选择版本

由社区的反馈还有我们的经验得知,利用最新的TypeScript功能的同时容纳旧版本的用户很困难。 TypeScript引入了叫做typesVersions的新特性来解决这种情况。

在TypeScript 3.1里使用Node模块解析时,TypeScript会读取package.json文件,找到它需要读取的文件,它首先会查看名字为typesVersions的字段。 一个带有typesVersions字段的package.json文件:

{
  "name": "package-name",
  "version": "1.0",
  "types": "./index.d.ts",
  "typesVersions": {
    ">=3.1": { "*": ["ts3.1/*"] }
  }
}

package.json告诉TypeScript去检查当前版本的TypeScript是否正在运行。 如果是3.1或以上的版本,它会找出你导入的包的路径,然后读取这个包里面的ts3.1文件夹里的内容。 这就是{ "*": ["ts3.1/*"] }的意义 - 如果你对路径映射熟悉,它们的工作方式类似。

因此在上例中,如果我们正在从"package-name"中导入,并且正在运行的TypeScript版本为3.1,我们会尝试从[...]/node_modules/package-name/ts3.1/index.d.ts开始解析。 如果是从package-name/foo导入,由会查找[...]/node_modules/package-name/ts3.1/foo.d.ts[...]/node_modules/package-name/ts3.1/foo/index.d.ts

那如果当前运行的TypeScript版本不是3.1呢? 如果typesVersions里没有能匹配上的版本,TypeScript将回退到查看types字段,因此TypeScript 3.0及之前的版本会重定向到[...]/node_modules/package-name/index.d.ts

匹配行为

TypeScript使用Node的semver ranges去决定编译器和语言版本。

多个字段

typesVersions支持多个字段,每个字段都指定了一个匹配范围。

{
  "name": "package-name",
  "version": "1.0",
  "types": "./index.d.ts",
  "typesVersions": {
    ">=3.2": { "*": ["ts3.2/*"] },
    ">=3.1": { "*": ["ts3.1/*"] }
  }
}

因为范围可能会重叠,因此指定的顺序是有意义的。 在上例中,尽管>=3.2>=3.1都匹配TypeScript 3.2及以上版本,反转它们的顺序将会有不同的结果,因此上例与下面的代码并不等同。

{
  "name": "package-name",
  "version": "1.0",
  "types": "./index.d.ts",
  "typesVersions": {
    // 注意,这样写不生效
    ">=3.1": { "*": ["ts3.1/*"] },
    ">=3.2": { "*": ["ts3.2/*"] }
  }
}

TypeScript 3.0

工程引用

TypeScript 3.0 引入了一个叫做工程引用的新概念。工程引用允许TypeScript工程依赖于其它TypeScript工程 - 特别要提的是允许tsconfig.json文件引用其它tsconfig.json文件。当指明了这些依赖后,就可以方便地将代码分割成单独的小工程,有助于TypeScript(以及周边的工具)了解构建顺序和输出结构。

TypeScript 3.0 还引入了一种新的tsc模式,即--build标记,它与工程引用同时运用可以加速构建TypeScript。

相关详情请阅读工程引用手册

剩余参数和展开表达式里的元组

TypeScript 3.0 增加了支持以元组类型与函数参数列表进行交互的能力。 如下:

有了这些特性后,便有可能将转换函数和它们参数列表的高阶函数变为强类型的。

带元组类型的剩余参数

当剩余参数里有元组类型时,元组类型被扩展为离散参数序列。 例如,如下两个声明是等价的:

declare function foo(...args: [number, string, boolean]): void;
declare function foo(args_0: number, args_1: string, args_2: boolean): void;

带有元组类型的展开表达式

在函数调用中,若最后一个参数是元组类型的展开表达式,那么这个展开表达式相当于元组元素类型的离散参数序列。

因此,下面的调用都是等价的:

const args: [number, string, boolean] = [42, "hello", true];
foo(42, "hello", true);
foo(args[0], args[1], args[2]);
foo(...args);

泛型剩余参数

剩余参数允许带有泛型类型,这个泛型类型被限制为是一个数组类型,类型推断系统能够推断这类泛型剩余参数里的元组类型。这样就可以进行高阶捕获和展开部分参数列表:

例子

declare function bind<T, U extends any[], V>(f: (x: T, ...args: U) => V, x: T): (...args: U) => V;

declare function f3(x: number, y: string, z: boolean): void;

const f2 = bind(f3, 42);  // (y: string, z: boolean) => void
const f1 = bind(f2, "hello");  // (z: boolean) => void
const f0 = bind(f1, true);  // () => void

f3(42, "hello", true);
f2("hello", true);
f1(true);
f0();

上例的f2声明,类型推断可以推断出number[string, boolean]void做为TUV

注意,如果元组类型是从参数序列中推断出来的,之后又扩展成参数列表,就像U那样,原来的参数名称会被用在扩展中(然而,这个名字没有语义上的意义且是察觉不到的)。

元组类型里的可选元素

元组类型现在允许在其元素类型上使用?后缀,表示这个元素是可选的:

例子

let t: [number, string?, boolean?];
t = [42, "hello", true];
t = [42, "hello"];
t = [42];

--strictNullChecks模式下,?修饰符会自动地在元素类型中包含undefined,类似于可选参数。

在元组类型的一个元素类型上使用?后缀修饰符来把它标记为可忽略的元素,且它右侧所有元素也同时带有了?修饰符。

当剩余参数推断为元组类型时,源码中的可选参数在推断出的类型里成为了可选元组元素。

带有可选元素的元组类型的length属性是表示可能长度的数字字面量类型的联合类型。 例如,[number, string?, boolean?]元组类型的length属性的类型是1 | 2 | 3

元组类型里的剩余元素

元组类型里最后一个元素可以是剩余元素,形式为...X,这里X是数组类型。 剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string[]]表示带有一个number元素和任意数量string类型元素的元组类型。

例子

function tuple<T extends any[]>(...args: T): T {
    return args;
}

const numbers: number[] = getArrayOfNumbers();
const t1 = tuple("foo", 1, true);  // [string, number, boolean]
const t2 = tuple("bar", ...numbers);  // [string, ...number[]]

这个带有剩余元素的元组类型的length属性类型是number

新的unknown类型

TypeScript 3.0引入了一个顶级的unknown类型。 对照于anyunknown是类型安全的。 任何值都可以赋给unknown,但是当没有类型断言或基于控制流的类型细化时unknown不可以赋值给其它类型,除了它自己和any外。 同样地,在unknown没有被断言或细化到一个确切类型之前,是不允许在其上进行任何操作的。

例子

// In an intersection everything absorbs unknown

type T00 = unknown & null;  // null
type T01 = unknown & undefined;  // undefined
type T02 = unknown & null & undefined;  // null & undefined (which becomes never)
type T03 = unknown & string;  // string
type T04 = unknown & string[];  // string[]
type T05 = unknown & unknown;  // unknown
type T06 = unknown & any;  // any

// In a union an unknown absorbs everything

type T10 = unknown | null;  // unknown
type T11 = unknown | undefined;  // unknown
type T12 = unknown | null | undefined;  // unknown
type T13 = unknown | string;  // unknown
type T14 = unknown | string[];  // unknown
type T15 = unknown | unknown;  // unknown
type T16 = unknown | any;  // any

// Type variable and unknown in union and intersection

type T20<T> = T & {};  // T & {}
type T21<T> = T | {};  // T | {}
type T22<T> = T & unknown;  // T
type T23<T> = T | unknown;  // unknown

// unknown in conditional types

type T30<T> = unknown extends T ? true : false;  // Deferred
type T31<T> = T extends unknown ? true : false;  // Deferred (so it distributes)
type T32<T> = never extends T ? true : false;  // true
type T33<T> = T extends never ? true : false;  // Deferred

// keyof unknown

type T40 = keyof any;  // string | number | symbol
type T41 = keyof unknown;  // never

// Only equality operators are allowed with unknown

function f10(x: unknown) {
    x == 5;
    x !== 10;
    x >= 0;  // Error
    x + 1;  // Error
    x * 2;  // Error
    -x;  // Error
    +x;  // Error
}

// No property accesses, element accesses, or function calls

function f11(x: unknown) {
    x.foo;  // Error
    x[5];  // Error
    x();  // Error
    new x();  // Error
}

// typeof, instanceof, and user defined type predicates

declare function isFunction(x: unknown): x is Function;

function f20(x: unknown) {
    if (typeof x === "string" || typeof x === "number") {
        x;  // string | number
    }
    if (x instanceof Error) {
        x;  // Error
    }
    if (isFunction(x)) {
        x;  // Function
    }
}

// Homomorphic mapped type over unknown

type T50<T> = { [P in keyof T]: number };
type T51 = T50<any>;  // { [x: string]: number }
type T52 = T50<unknown>;  // {}

// Anything is assignable to unknown

function f21<T>(pAny: any, pNever: never, pT: T) {
    let x: unknown;
    x = 123;
    x = "hello";
    x = [1, 2, 3];
    x = new Error();
    x = x;
    x = pAny;
    x = pNever;
    x = pT;
}

// unknown assignable only to itself and any

function f22(x: unknown) {
    let v1: any = x;
    let v2: unknown = x;
    let v3: object = x;  // Error
    let v4: string = x;  // Error
    let v5: string[] = x;  // Error
    let v6: {} = x;  // Error
    let v7: {} | null | undefined = x;  // Error
}

// Type parameter 'T extends unknown' not related to object

function f23<T extends unknown>(x: T) {
    let y: object = x;  // Error
}

// Anything but primitive assignable to { [x: string]: unknown }

function f24(x: { [x: string]: unknown }) {
    x = {};
    x = { a: 5 };
    x = [1, 2, 3];
    x = 123;  // Error
}

// Locals of type unknown always considered initialized

function f25() {
    let x: unknown;
    let y = x;
}

// Spread of unknown causes result to be unknown

function f26(x: {}, y: unknown, z: any) {
    let o1 = { a: 42, ...x };  // { a: number }
    let o2 = { a: 42, ...x, ...y };  // unknown
    let o3 = { a: 42, ...x, ...y, ...z };  // any
}

// Functions with unknown return type don't need return expressions

function f27(): unknown {
}

// Rest type cannot be created from unknown

function f28(x: unknown) {
    let { ...a } = x;  // Error
}

// Class properties of type unknown don't need definite assignment

class C1 {
    a: string;  // Error
    b: unknown;
    c: any;
}

在JSX里支持defaultProps

TypeScript 2.9和之前的版本不支持在JSX组件里使用React的defaultProps声明。 用户通常不得不将属性声明为可选的,然后在render里使用非null的断言,或者在导出之前对组件的类型使用类型断言。

TypeScript 3.0在JSX命名空间里支持一个新的类型别名LibraryManagedAttributes。 这个助手类型定义了在检查JSX表达式之前在组件Props上的一个类型转换;因此我们可以进行定制:如何处理提供的props与推断props之间的冲突,推断如何映射,如何处理可选性以及不同位置的推断如何结合在一起。

我们可以利用它来处理React的defaultProps以及propTypes

export interface Props {
    name: string;
}

export class Greet extends React.Component<Props> {
    render() {
        const { name } = this.props;
        return <div>Hello {name.toUpperCase()}!</div>;
    }
    static defaultProps = { name: "world"};
}

// Type-checks! No type assertions needed!
let el = <Greet />

说明

defaultProps的确切类型

默认类型是从defaultProps属性的类型推断而来。如果添加了显式的类型注释,比如static defaultProps: Partial<Props>;,编译器无法识别哪个属性具有默认值(因为defaultProps类型包含了Props的所有属性)。

使用static defaultProps: Pick<Props, "name">;做为显式的类型注释,或者不添加类型注释。

对于函数组件(之前叫做SFC),使用ES2015默认的初始化器:

function Greet({ name = "world" }: Props) {
    return <div>Hello {name.toUpperCase()}!</div>;
}

@types/React的改动

仍需要在@types/ReactJSX命名空间上添加LibraryManagedAttributes定义。

/// <reference lib="..." />指令

TypeScript增加了一个新的三斜线指令(/// <reference lib="name" />),允许一个文件显式地包含一个已知的内置_lib_文件。

内置的_lib_文件的引用和_tsconfig.json_里的编译器选项"lib"相同(例如,使用lib="es2015"而不是lib="lib.es2015.d.ts"等)。

当你写的声明文件依赖于内置类型时,例如DOM APIs或内置的JS运行时构造函数如SymbolIterable,推荐使用三斜线引用指令。之前,这个.d.ts文件不得不添加重覆的类型声明。

例子

在某个文件里使用 /// <reference lib="es2017.string" />等同于指定--lib es2017.string编译选项。

/// <reference lib="es2017.string" />

"foo".padStart(4);

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文件里。

TypeScript 2.8

有条件类型

TypeScript 2.8引入了_有条件类型_,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y

上面的类型意思是,若T能够赋值给U,那么类型是X,否则为Y

有条件的类型T extends U ? X : Y或者_解析_为X,或者_解析_为Y,再或者_延迟_解析,因为它可能依赖一个或多个类型变量。 是否直接解析或推迟取决于:

  • 首先,令T'U'分别为TU的实例,并将所有类型参数替换为any,如果T'不能赋值给U',则将有条件的类型解析成Y。直观上讲,如果最宽泛的T的实例不能赋值给最宽泛的U的实例,那么我们就可以断定不存在可以赋值的实例,因此可以解析为Y
  • 其次,针对每个在U内由推断声明引入的类型变量,依据从T推断到U来收集一组候选类型(使用与泛型函数类型推断相同的推断算法)。对于给定的推断类型变量V,如果有候选类型是从协变的位置上推断出来的,那么V的类型是那些候选类型的联合。反之,如果有候选类型是从逆变的位置上推断出来的,那么V的类型是那些候选类型的交叉类型。否则V的类型是never
  • 然后,令T''T的一个实例,所有推断的类型变量用上一步的推断结果替换,如果T''_明显可赋值_给U,那么将有条件的类型解析为X。除去不考虑类型变量的限制之外,_明显可赋值_的关系与正常的赋值关系一致。直观上,当一个类型明显可赋值给另一个类型,我们就能够知道它可以赋值给那些类型的_所有_实例。
  • 否则,这个条件依赖于一个或多个类型变量,有条件的类型解析被推迟进行。

例子

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

分布式有条件类型

如果有条件类型里待检查的类型是naked type parameter,那么它也被称为“分布式有条件类型”。 分布式有条件类型在实例化时会自动分发成联合类型。 例如,实例化T extends U ? X : YT的类型为A | B | C,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

例子

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T12 = TypeName<string | string[] | undefined>;  // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>;  // "object"

T extends U ? X : Y的实例化里,对T的引用被解析为联合类型的一部分(比如,T指向某一单个部分,在有条件类型分布到联合类型之后)。 此外,在X内对T的引用有一个附加的类型参数约束U(例如,T被当成在X内可赋值给U)。

例子

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[]>;  // BoxedArray<number>;
type T22 = Boxed<string | number[]>;  // BoxedValue<string> | BoxedArray<number>;

注意在Boxed<T>true分支里,T有个额外的约束any[],因此它适用于T[number]数组元素类型。同时也注意一下有条件类型是如何分布成联合类型的。

有条件类型的分布式的属性可以方便地用来_过滤_联合类型:

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>;  // string | number
type T33 = Filter<string | number | (() => void), Function>;  // () => void

type NonNullable<T> = Diff<T, null | undefined>;  // Remove null and undefined from T

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

function f1<T>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}

有条件类型与映射类型结合时特别有用:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

与联合类型和交叉类型相似,有条件类型不允许递归地引用自己。比如下面的错误。

例子

type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;  // Error

有条件类型中的类型推断

现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer

例如,下面代码会提取函数类型的返回值类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

下面的例子解释了在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

相似地,在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

当推断具有多个调用签名(例如函数重载类型)的类型时,用_最后_的签名(大概是最自由的包含所有情况的签名)进行推断。 无法根据参数类型列表来解析重载。

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>;  // string | number

无法在正常类型参数的约束子语句中使用infer声明:

type ReturnType<T extends (...args: any[]) => infer R> = R;  // 错误,不支持

但是,可以这样达到同样的效果,在约束里删掉类型变量,用有条件类型替换:

type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;

预定义的有条件类型

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

Example

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

注意:Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。我们没有增加Omit<T, K>类型,因为它可以很容易的用Pick<T, Exclude<keyof T, K>>来表示。

改进对映射类型修饰符的控制

映射类型支持在属性上添加readonly?修饰符,但是它们不支持_移除_修饰符。 这对于同态映射类型有些影响,因为同态映射类型默认保留底层类型的修饰符。

TypeScript 2.8为映射类型增加了增加或移除特定修饰符的能力。 特别地,映射类型里的readonly?属性修饰符现在可以使用+-前缀,来表示修饰符是添加还是移除。

例子

type MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] };  // 移除readonly和?
type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] };  // 添加readonly和?

不带+-前缀的修饰符与带+前缀的修饰符具有相同的作用。因此上面的ReadonlyPartial<T>类型与下面的一致

type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] };  // 添加readonly和?

利用这个特性,lib.d.ts现在有了一个新的Required<T>类型。 它移除了T的所有属性的?修饰符,因此所有属性都是必需的。

例子

type Required<T> = { [P in keyof T]-?: T[P] };

注意在--strictNullChecks模式下,当同态映射类型移除了属性底层类型的?修饰符,它同时也移除了那个属性上的undefined类型:

例子

type Foo = { a?: string };  // 等同于 { a?: string | undefined }
type Bar = Required<Foo>;  // 等同于 { a: string }

改进交叉类型上的keyof

TypeScript 2.8作用于交叉类型的keyof被转换成作用于交叉成员的keyof的联合。 换句话说,keyof (A & B)会被转换成keyof A | keyof B。 这个改动应该能够解决keyof表达式推断不一致的问题。

例子

type A = { a: string };
type B = { b: string };

type T1 = keyof (A & B);  // "a" | "b"
type T2<T> = keyof (T & B);  // keyof T | "b"
type T3<U> = keyof (A & U);  // "a" | keyof U
type T4<T, U> = keyof (T & U);  // keyof T | keyof U
type T5 = T2<A>;  // "a" | "b"
type T6 = T3<B>;  // "a" | "b"
type T7 = T4<A, B>;  // "a" | "b"

更好的处理.js文件中的命名空间模式

TypeScript 2.8加强了识别.js文件里的命名空间模式。 JavaScript顶层的空对象字面量声明,就像函数和类,会被识别成命名空间声明。

var ns = {};     // recognized as a declaration for a namespace `ns`
ns.constant = 1; // recognized as a declaration for var `constant`

顶层的赋值应该有一致的行为;也就是说,varconst声明不是必需的。

app = {}; // does NOT need to be `var app = {}`
app.C = class {
};
app.f = function() {
};
app.prop = 1;

立即执行的函数表达式做为命名空间

立即执行的函数表达式返回一个函数,类或空的对象字面量,也会被识别为命名空间:

var C = (function () {
  function C(n) {
    this.p = n;
  }
  return C;
})();
C.staticProperty = 1;

默认声明

“默认声明”允许引用了声明的名称的初始化器出现在逻辑或的左边:

my = window.my || {};
my.app = my.app || {};

原型赋值

你可以把一个对象字面量直接赋值给原型属性。独立的原型赋值也可以:

var C = function (p) {
  this.p = p;
};
C.prototype = {
  m() {
    console.log(this.p);
  }
};
C.prototype.q = function(r) {
  return this.p === r;
};

嵌套与合并声明

现在嵌套的层次不受限制,并且多文件之间的声明合并也没有问题。以前不是这样的。

var app = window.app || {};
app.C = class { };

各文件的JSX工厂

TypeScript 2.8增加了使用@jsx dom指令为每个文件设置JSX工厂名。 JSX工厂也可以使用--jsxFactory编译参数设置(默认值为React.createElement)。TypeScript 2.8你可以基于文件进行覆写。

例子

/** @jsx dom */
import { dom } from "./renderer"
<h></h>

生成:

var renderer_1 = require("./renderer");
renderer_1.dom("h", null);

本地范围的JSX命名空间

JSX类型检查基于JSX命名空间里的定义,比如JSX.Element用于JSX元素的类型,JSX.IntrinsicElements用于内置的元素。 在TypeScript 2.8之前JSX命名空间被视为全局命名空间,并且一个工程只允许存在一个。 TypeScript 2.8开始,JSX命名空间将在jsxNamespace下面查找(比如React),允许在一次编译中存在多个jsx工厂。 为了向后兼容,全局的JSX命名空间被当做回退选项。 使用独立的@jsx指令,每个文件可以有自己的JSX工厂。

新的--emitDeclarationsOnly

--emitDeclarationsOnly允许_仅_生成声明文件;使用这个标记.js/.jsx输出会被跳过。当使用其它的转换工具如Babel处理.js输出的时候,可以使用这个标记。

TypeScript 2.7

TypeScript 2.7

常量名属性

TypeScript 2.7 新增了以常量(包括ECMAScript symbols)作为类属性名的类型推断支持。

例子

// Lib
export const SERIALIZE = Symbol("serialize-method-key");

export interface Serializable {
    [SERIALIZE](obj: {}): string;
}
// consumer
import { SERIALIZE, Serializable } from "lib";

class JSONSerializableItem implements Serializable {
    [SERIALIZE](obj: {}) {
        return JSON.stringify(obj);
    }
}

这同样适用于数字和字符串的字面量

例子

const Foo = "Foo";
const Bar = "Bar";

let x = {
    [Foo]: 100,
    [Bar]: "hello",
};

let a = x[Foo]; // a类型为'number'; 在之前版本,类型为'number | string',现在可以追踪到类型
let b = x[Bar]; // b类型为'string';

unique symbol类型

为了将symbol变量视作有唯一值的字面量,我们新增了类型unique symbolunique symbolsymbol的子类型,仅由调用Symbol()Symbol.for()或明确的类型注释生成。 该类型只允许在const声明或者 readonly static 属性声明中使用。如果要引用某个特定的unique symbol变量,你必须使用typeof操作符。 每个对unique symbols的引用都意味着一个完全唯一的声明身份,与被引用的变量声明绑定。

例子

// Works
declare const Foo: unique symbol;

// Error! 'Bar'不是const声明的
let Bar: unique symbol = Symbol();

// Works - 对变量Foo的引用,它的声明身份与Foo绑定
let Baz: typeof Foo = Foo;

// Also works.
class C {
    static readonly StaticSymbol: unique symbol = Symbol();
}

因为每个unique symbols都有个完全独立的身份,因此两个unique symbols类型之间不能赋值或比较。

Example

const Foo = Symbol();
const Bar = Symbol();

// Error: 不能比较两个unique symbols.
if (Foo === Bar) {
    // ...
}

更严格的类属性检查

TypeScript 2.7引入了一个新的控制严格性的标记--strictPropertyInitialization。 使用这个标记后,TypeScript要求类的所有实例属性在构造函数里或属性初始化器中都得到初始化。比如:

class C {
    foo: number;
    bar = "hello";
    baz: boolean;
//  ~~~
//  Error! Property 'baz' has no initializer and is not assigned directly in the constructor.
    constructor() {
        this.foo = 42;
    }
}

上例中,baz从未被赋值,因此TypeScript报错了。 如果我们的本意就是让baz可以为undefined,那么应该声明它的类型为boolean | undefined

在某些场景下,属性会被间接地初始化(使用辅助方法或依赖注入库)。 这种情况下,你可以在属性上使用_显式赋值断言_(definite assignment assertion modifiers)来帮助类型系统识别类型(下面会讨论)

class C {
    foo!: number;
    // ^
    // Notice this exclamation point!
    // This is the "definite assignment assertion" modifier.
    constructor() {
        this.initialize();
    }

    initialize() {
        this.foo = 0;
    }
}

注意,--strictPropertyInitialization会在其它--strict模式标记下被启用,这可能会影响你的工程。 你可以在tsconfig.jsoncompilerOptions里将strictPropertyInitialization设置为false, 或者在命令行上将--strictPropertyInitialization设置为false来关闭检查。

显式赋值断言

显式赋值断言允许你在实例属性和变量声明之后加一个感叹号!,来告诉TypeScript这个变量确实已被赋值,即使TypeScript不能分析出这个结果。

例子

let x: number;
initialize();
console.log(x + x);
//          ~   ~
// Error! Variable 'x' is used before being assigned.

function initialize() {
    x = 10;
}

使用显式类型断言在x的声明后加上!,Typescript可以认为变量x确实已被赋值

// Notice the '!'
let x!: number;
initialize();

// No error!
console.log(x + x);

function initialize() {
    x = 10;
}

在某种意义上,显式类型断言运算符是非空断言运算符(在表达式后缀的!)的对偶,就像下面这个例子

let x: number;
initialize();

// No error!
console.log(x! + x!);

function initialize() {
    x = 10;

在上面的例子中,我们知道x都会被初始化,因此使用显式类型断言比使用非空断言更合适。

固定长度元组

TypeScript 2.6之前,[number, string, string]被当作[number, string]的子类型。 这对于TypeScript的结构性而言是合理的——[number, string, string]的前两个元素各自是[number, string]里前两个元素的子类型。 但是,我们注意到在在实践中的大多数情形下,这并不是开发者所希望的。

在TypeScript 2.7中,具有不同元数的元组不再允许相互赋值。感谢Tycho Grouwstra提交的PR,元组类型现在会将它们的元数编码进它们对应的length属性的类型里。原理是利用数字字面量类型区分出不同长度的元组。

概念上讲,你可以把[number, string]类型等同于下面的NumStrTuple声明:

interface NumStrTuple extends Array<number | string> {
    0: number;
    1: string;
    length: 2; // 注意length的类型是字面量'2',而不是'number'
}

请注意,这是一个破坏性改动。 如果你想要和以前一样,让元组仅限制最小长度,那么你可以使用一个类似的声明但不显式指定length属性,这样length属性的类型就会回退为number

interface MinimumNumStrTuple extends Array<number | string> {
    0: number;
    1: string;
}

注:这并不意味着元组是不可变长的数组,而仅仅是一个约定。

更优的对象字面量推断

TypeScript 2.7改进了在同一上下文中的多对象字面量的类型推断。 当多个对象字面量类型组成一个联合类型,TypeScript现在会将它们_规范化_为一个对象类型,该对象类型包含联合类型中的每个对象的所有属性,以及属性对应的推断类型。

考虑这样的情形:

const obj = test ? { text: "hello" } : {};  // { text: string } | { text?: undefined }
const s = obj.text;  // string | undefined

以前obj会被推断为{},第二行会报错因为obj没有属性。但这显然并不理想。

例子

// let obj: { a: number, b: number } |
//     { a: string, b?: undefined } |
//     { a?: undefined, b?: undefined }
let obj = [{ a: 1, b: 2 }, { a: "abc" }, {}][0];
obj.a;  // string | number | undefined
obj.b;  // number | undefined

多个对象字面量中的同一属性的所有推断类型,会合并成一个规范化的联合类型:

declare function f<T>(...items: T[]): T;
// let obj: { a: number, b: number } |
//     { a: string, b?: undefined } |
//     { a?: undefined, b?: undefined }
let obj = f({ a: 1, b: 2 }, { a: "abc" }, {});
obj.a;  // string | number | undefined
obj.b;  // number | undefined

结构相同的类和instanceof表达式的处理方式改进

TypeScript 2.7对联合类型中结构相同的类和instanceof表达式的处理方式改进如下:

  • 联合类型中,结构相同的不同类都会保留(而不是只保留一个)
  • 联合类型中的子类型简化仅在一种情况下发生——若一个类继承自联合类型中另一个类,该子类会被简化。
  • 用于类型检查的instanceof操作符基于继承关系来判断,而不是结构兼容来判断。

这意味着联合类型和instanceof能够区分结构相同的类。

例子

class A {}
class B extends A {}
class C extends A {}
class D extends A { c: string }
class E extends D {}

let x1 = !true ? new A() : new B();  // A
let x2 = !true ? new B() : new C();  // B | C (previously B)
let x3 = !true ? new C() : new D();  // C | D (previously C)

let a1 = [new A(), new B(), new C(), new D(), new E()];  // A[]
let a2 = [new B(), new C(), new D(), new E()];  // (B | C | D)[] (previously B[])

function f1(x: B | C | D) {
    if (x instanceof B) {
        x;  // B (previously B | D)
    }
    else if (x instanceof C) {
        x;  // C
    }
    else {
        x;  // D (previously never)
    }
}

in运算符实现类型保护

in运算符现在会起到类型细化的作用。

对于一个n in x的表达式,当n是一个字符串字面量或者字符串字面量类型,并且x是一个联合类型: 在值为"true"的分支中,x会有一个推断出来可选或被赋值的属性n;在值为"false"的分支中,x根据推断仅有可选的属性n或没有属性n

例子

interface A { a: number };
interface B { b: string };

function foo(x: A | B) {
    if ("a" in x) {
        return x.a;
    }
    return x.b; // 此时x的类型推断为B, 属性a不存在
}

使用标记--esModuleInterop引入非ES模块

在TypeScript 2.7使用--esModuleInterop标记后,为_CommonJS/AMD/UMD_模块生成基于__esModule指示器的命名空间记录。这次更新使得TypeScript编译后的输出与Babel的输出更加接近。

之前版本中,TypeScript处理_CommonJS/AMD/UMD_模块的方式与处理ES6模块一致,导致了一些问题,比如:

  • TypeScript之前处理CommonJS/AMD/UMD模块的命名空间导入(如import * as foo from "foo")时等同于const foo = require("foo")。这样做很简单,但如果引入的主要对象(比如这里的foo)是基本类型、类或者函数,就有问题。ECMAScript标准规定了命名空间记录是一个纯粹的对象,并且引入的命名空间(比如前面的foo)应该是不可调用的,然而在TypeScript却中可以。
  • 同样地,一个CommonJS/AMD/UMD模块的默认导入(如import d from "foo")被处理成等同于 const d = require("foo").default的形式。然而现在大多数可用的CommonJS/AMD/UMD模块并没有默认导出,导致这种引入语句在实践中不适用于非ES模块。比如 import fs from "fs" or import express from "express" 都不可用。

在使用标签--esModuleInterop后,这两个问题都得到了解决:

  • 命名空间导入(如import * as foo from "foo")的对象现在被修正为不可调用的。调用会报错。
  • 对CommonJS/AMD/UMD模块可以使用默认导入(如import d from "foo")且能正常工作了。

注: 这个新特性有可能对现有的代码产生破坏,因此以标记的方式引入。但无论是新项目还是之前的项目,我们都强烈建议使用它。对于之前的项目,命名空间导入 (import * as express from "express"; express();) 需要被改写成默认引入 (import express from "express"; express();).

例子

使用 --esModuleInterop 后,会生成两个新的辅助量 __importStar and __importDefault ,分别对应导入*和导入default,比如这样的输入:

import * as foo from "foo";
import b from "bar";

会生成:

"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
}
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
}
exports.__esModule = true;
var foo = __importStar(require("foo"));
var bar_1 = __importDefault(require("bar"));

数字分隔符

TypeScript 2.7支持ECMAScript的数字分隔符提案。 这个特性允许用户在数字之间使用下划线_来对数字分组。

const million = 1_000_000;
const phone = 555_734_2231;
const bytes = 0xFF_0C_00_FF;
const word = 0b1100_0011_1101_0001;

--watch模式下具有更简洁的输出

在TypeScript的--watch模式下进行重新编译后会清屏。 这样就更方便阅读最近这次编译的输出信息。

更漂亮的--pretty输出

TypeScript的--pretty标记可以让错误信息更易阅读和管理。 我们对这个功能进行了两个主要的改进。 首先,--pretty对文件名,诊段代码和行数添加了颜色(感谢Joshua Goldberg)。 其次,格式化了文件名和位置,以便于在常用的终端里使用Ctrl+Click,Cmd+Click,Alt+Click等来跳转到编译器里的相应位置。

TypeScript 2.6

严格函数类型

TypeScript 2.6引入了新的类型检查选项,--strictFunctionTypes--strictFunctionTypes选项是--strict系列选项之一,也就是说 --strict模式下它默认是启用的。你可以通过在命令行或tsconfig.json中设置--strictFunctionTypes false来单独禁用它。

--strictFunctionTypes启用时,函数类型参数的检查是_抗变(contravariantly)_而非_双变(bivariantly)_的。关于变体 (variance) 对于函数类型意义的相关背景,请查看协变(covariance)和抗变(contravariance)是什么?

这一更严格的检查应用于除方法或构造函数声明以外的所有函数类型。方法被专门排除在外是为了确保带泛型的类和接口(如Array<T>)总体上仍然保持协变。

考虑下面这个 Animal 是 Dog 和 Cat 的父类型的例子:

declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;
f1 = f2;  // 启用 --strictFunctionTypes 时错误
f2 = f1;  // 正确
f2 = f3;  // 错误

第一个赋值语句在默认的类型检查模式中是允许的,但是在严格函数类型模式下会被标记错误。 通俗地讲,默认模式允许这么赋值,因为它_可能是_合理的,而严格函数类型模式将它标记为错误,因为它不能_被证明_合理。 任何一种模式中,第三个赋值都是错误的,因为它_永远不_合理。

用另一种方式来描述这个例子则是,默认类型检查模式中T在类型(x: T) => void是_双变的_(也即协变_或_抗变),但在严格函数类型模式中T是_抗变_的。

例子

interface Comparer<T> {
    compare: (a: T, b: T) => number;
}

declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;

animalComparer = dogComparer;  // 错误
dogComparer = animalComparer;  // 正确

现在第一个赋值是错误的。更明确地说,Comparer<T>中的T因为仅在函数类型参数的位置被使用,是抗变的。

另外,注意尽管有的语言(比如C#和Scala)要求变体标注(variance annotations)(out/in+/-),而由于TypeScript的结构化类型系统,它的变体是由泛型中的类型参数的实际使用自然得出的。

注意:

启用--strictFunctionTypes时,如果compare被声明为方法,则第一个赋值依然是被允许的。 更明确的说,Comparer<T>中的T因为仅在方法参数的位置被使用所以是双变的。

interface Comparer<T> {
    compare(a: T, b: T): number;
}

declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;

animalComparer = dogComparer;  // 正确,因为双变
dogComparer = animalComparer;  // 正确

TypeScript 2.6 还改进了与抗变位置相关的类型推导:

function combine<T>(...funcs: ((x: )=> void)[]): (x: T) => void {
    return x => {
        for (const f of funcs) f(x);
    }
}

function animalFunc(x: Animal) {}
function dogFunc(x: Dog) {}

let combined = combine(animalFunc,dogFunc);  // (x: Dog) => void

这上面所有T的推断都来自抗变的位置,由此我们得出T的_最普遍子类型_。 这与从协变位置推导出的结果恰恰相反,从协变位置我们得出的是_最普遍超类型_。

缓存模块中的标签模板对象

TypeScript 2.6修复了标签字符串模板的输出,以更好地遵循ECMAScript标准。 根据ECMAScript 标准,每一次获取模板标签的值时,应该将_同一个_模板字符串数组对象 (同一个 TemplateStringArray) 作为第一个参数传递。 在 TypeScript 2.6 之前,每一次生成的都是全新的模板对象。 虽然字符串的内容是一样的,这样的输出会影响通过识别字符串来实现缓存失效的库,比如 lit-html

例子

export function id(x: TemplateStringsArray) {
    return x;
}

export function templateObjectFactory() {
    return id`hello world`;
}

let result = templateObjectFactory() === templateObjectFactory(); // TS 2.6 为 true

编译后的代码:

"use strict";
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
    if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
    return cooked;
};

function id(x) {
    return x;
}

var _a;
function templateObjectFactory() {
    return id(_a || (_a = __makeTemplateObject(["hello world"], ["hello world"])));
}

var result = templateObjectFactory() === templateObjectFactory();

注意:这一改变引入了新的工具函数,__makeTemplateObject; 如果你在搭配使用--importHelperstslib,需要更新到 1.8 或更高版本。

本地化的命令行诊断消息

TypeScript 2.6 npm包加入了13种语言的诊断消息本地化版本。 命令行中本地化消息会在使用--locale选项时显示。

例子

俄语显示的错误消息:

c:\ts>tsc --v
Version 2.6.1

c:\ts>tsc --locale ru --pretty c:\test\a.ts

../test/a.ts(1,5): error TS2322: Тип ""string"" не может быть назначен для типа "number".

1 var x: number = "string";
      ~

中文显示的帮助信息:

PS C:\ts> tsc --v
Version 2.6.1

PS C:\ts> tsc --locale zh-cn
版本 2.6.1
语法:tsc [选项] [文件 ...]

示例:tsc hello.ts
    tsc --outFile file.js file.ts
    tsc @args.txt

选项:
 -h, --help                    打印此消息。
 --all                         显示所有编译器选项。
 -v, --version                 打印编译器的版本。
 --init                        初始化 TypeScript 项目并创建 tsconfig.json 文件。
 -p 文件或目录, --project 文件或目录     编译给定了其配置文件路径或带 "tsconfig.json" 的文件夹路径的项目。
 --pretty                      使用颜色和上下文风格化错误和消息(实验)。
 -w, --watch                   监视输入文件。
 -t 版本, --target 版本            指定 ECMAScript 目标版本:"ES3"(默认)、"ES5"、"ES2015"、"ES2016"、"ES2017" 或 "ESNEXT"。
 -m 种类, --module 种类            指定模块代码生成:"none"、"commonjs"、"amd"、"system"、"umd"、"es2015"或 "ESNext"。
 --lib                         指定要在编译中包括的库文件:
                                 'es5' 'es6' 'es2015' 'es7' 'es2016' 'es2017' 'esnext' 'dom' 'dom.iterable' 'webworker' 'scripthost' 'es2015.core' 'es2015.collection' 'es2015.generator' 'es2015.iterable' 'es2015.promise' 'es2015.proxy' 'es2015.reflect' 'es2015.symbol' 'es2015.symbol.wellknown' 'es2016.array.include' 'es2017.object' 'es2017.sharedmemory' 'es2017.string' 'es2017.intl' 'esnext.asynciterable'
 --allowJs                     允许编译 JavaScript 文件。
 --jsx 种类                      指定 JSX 代码生成:"preserve"、"react-native" 或 "react"。 -d, --declaration             生成相应的 ".d.ts" 文件。
 --sourceMap                   生成相应的 ".map" 文件。
 --outFile 文件                  连接输出并将其发出到单个文件。
 --outDir 目录                   将输出结构重定向到目录。
 --removeComments              请勿将注释发出到输出。
 --noEmit                      请勿发出输出。
 --strict                      启用所有严格类型检查选项。
 --noImplicitAny               对具有隐式 "any" 类型的表达式和声明引发错误。
 --strictNullChecks            启用严格的 NULL 检查。
 --strictFunctionTypes         对函数类型启用严格检查。
 --noImplicitThis              在带隐式"any" 类型的 "this" 表达式上引发错误。
 --alwaysStrict                以严格模式进行分析,并为每个源文件发出 "use strict" 指令。
 --noUnusedLocals              报告未使用的局部变量上的错误。
 --noUnusedParameters          报告未使用的参数上的错误。
 --noImplicitReturns           在函数中的所有代码路径并非都返回值时报告错误。
 --noFallthroughCasesInSwitch  报告 switch 语句中遇到 fallthrough 情况的错误。
 --types                       要包含在编译中类型声明文件。
 @<文件>                         从文件插入命令行选项和文件。

通过 '// @ts-ignore' 注释隐藏 .ts 文件中的错误

TypeScript 2.6支持在.ts文件中通过在报错一行上方使用// @ts-ignore来忽略错误。

例子

if (false) {
    // @ts-ignore:无法被执行的代码的错误
    console.log("hello");
}

// @ts-ignore注释会忽略下一行中产生的所有错误。 建议实践中在@ts-ignore之后添加相关提示,解释忽略了什么错误。

请注意,这个注释仅会隐藏报错,并且我们建议你_极少_使用这一注释。

更快的 tsc --watch

TypeScript 2.6 带来了更快的--watch实现。 新版本优化了使用ES模块的代码的生成和检查。 在一个模块文件中检测到的改变_只_会使改变的模块,以及依赖它的文件被重新生成,而不再是整个项目。 有大量文件的项目应该从这一改变中获益最多。

这一新的实现也为tsserver中的监听带来了性能提升。 监听逻辑被完全重写以更快响应改变事件。

只写的引用现在会被标记未使用

TypeScript 2.6加入了修正的--noUnusedLocals--noUnusedParameters编译选项实现。 只被写但从没有被读的声明现在会被标记未使用。

例子

下面nm都会被标记为未使用,因为它们的值从未被_读取_。之前 TypeScript 只会检查它们的值是否被_引用_。

function f(n: number) {
    n = 0;
}

class C {
    private m: number;
    constructor() {
        this.m = 0;
    }
}

另外仅被自己内部调用的函数也会被认为是未使用的。

例子

function f() {
    f(); // 错误:'f' 被声明,但它的值从未被使用
}

TypeScript 2.5

可选的catch语句变量

得益于@tinganho所做的工作,TypeScript 2.5实现了一个新的ECMAScript特性,允许用户省略catch语句中的变量。 例如,当使用JSON.parse时,你可能需要将对应的函数调用放在try / catch中,但是最后可能并不会用到输入有误时会抛出的SyntaxError(语法错误)。

let input = "...";
try {
    JSON.parse(input);
}
catch {
    // ^ 注意我们的 `catch` 语句并没有声明一个变量
    console.log("传入的 JSON 不合法\n\n" + input)
}

checkJs/@ts-check 模式中的类型断言/转换语法

TypeScript 2.5 引入了在使用纯 JavaScript 的项目中断言表达式类型的能力。对应的语法是/** @type {...} */标注注释后加上被圆括号括起来,类型需要被重新演算的表达式。举例:

var x = /** @type {SomeType} */ (AnyParenthesizedExpression);

包去重和重定向

在 TypeScript 2.5 中使用Node模块解析策略进行导入时,编译器现在会检查文件是否来自 "相同" 的包。如果一个文件所在的包的package.json包含了与之前读取的包相同的nameversion,那么TypeScript会将它重定向到最顶层的包。这可以解决两个包可能会包含相同的类声明,但因为包含private成员导致他们在结构上不兼容的问题.

这也带来一个额外的好处,可以通过避免从重复的包中加载.d.ts文件减少内存使用和编译器及语言服务的运行时计算.

--preserveSymlinks(保留符号链接)编译器选项

TypeScript 2.5带来了preserveSymlinks选项,它对应了Node.js 中 --preserve-symlinks选项的行为。这一选项也会带来和Webpack的resolve.symlinks选项相反的行为(也就是说,将TypeScript的preserveSymlinks选项设置为true对应了将Webpack的resolve.symlinks选项设为false,反之亦然)。

在这一模式中,对于模块和包的引用(比如import语句和/// <reference type=".." />指令)都会以相对符号链接文件的位置被解析,而不是相对于符号链接解析到的路径。更具体的例子,可以参考Node.js网站的文档

TypeScript 2.4

动态导入表达式

动态的import表达式是一个新特性,它属于ECMAScript的一部分,允许用户在程序的任何位置异步地请求某个模块。

这意味着你可以有条件地延迟加载其它模块和库。 例如下面这个async函数,它仅在需要的时候才导入工具库:

async function getZipFile(name: string, files: File[]): Promise<File> {
    const zipUtil = await import('./utils/create-zip-file');
    const zipContents = await zipUtil.getContentAsBlob(files);
    return new File(zipContents, name);
}

许多bundlers工具已经支持依照这些import表达式自动地分割输出,因此可以考虑使用这个新特性并把输出模块目标设置为esnext

字符串枚举

TypeScript 2.4现在支持枚举成员变量包含字符串构造器。

enum Colors {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

需要注意的是字符串枚举成员不能被反向映射到枚举成员的名字。 换句话说,你不能使用Colors["RED"]来得到"Red"

增强的泛型推断

TypeScript 2.4围绕着泛型的推断方式引入了一些很棒的改变。

返回类型作为推断目标

其一,TypeScript能够推断调用的返回值类型。 这可以优化你的体验和方便捕获错误。 如下所示:

function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[] {
    return a => a.map(f);
}

const lengths: (a: string[]) => number[] = arrayMap(s => s.length);

下面是一个你可能会见到的出错了的例子:

let x: Promise<string> = new Promise(resolve => {
    resolve(10);
    //      ~~ Error!
});

从上下文类型中推断类型参数

在TypeScript 2.4之前,在下面的例子里:

let f: <T>(x: T) => T = y => y;

y将会具有any类型。 这意味着虽然程序会检查类型,但是你却可以使用y做任何事情,就比如:

let f: <T>(x: T) => T = y => y() + y.foo.bar;

这个例子实际上并不是类型安全的。

在TypeScript 2.4里,右手边的函数会隐式地获得类型参数,并且y的类型会被推断为那个类型参数的类型。

如果你使用y的方式是这个类型参数所不支持的,那么你会得到一个错误。 在这个例子里,T的约束是{}(隐式地),所以在最后一个例子里会出错。

对泛型函数进行更严格的检查

TypeScript在比较两个单一签名的类型时会尝试统一类型参数。 因此,在涉及到两个泛型签名的时候会进行更严格的检查,这就可能发现一些bugs。

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

function f(a: A, b: B) {
    a = b;  // Error
    b = a;  // Ok
}

回调参数的严格抗变

TypeScript一直是以双变(bivariant)的方式来比较参数。 这样做有很多原因,总体上来说这不会有什么大问题直到我们发现它应用在PromiseObservable上时有些副作用。

TypeScript 2.4在处理两个回调类型时引入了收紧机制。例如:

interface Mappable<T> {
    map<U>(f: (x: T) => U): Mappable<U>;
}

declare let a: Mappable<number>;
declare let b: Mappable<string | number>;

a = b;
b = a;

在TypeScript 2.4之前,它会成功执行。 当关联map的类型时,TypeScript会双向地关联它们的类型(例如f的类型)。 当关联每个f的类型时,TypeScript也会双向地关联那些参数的类型。

TS 2.4里关联map的类型时,TypeScript会检查是否每个参数都是回调类型,如果是的话,它会确保那些参数根据它所在的位置以抗变(contravariant)地方式进行检查。

换句话说,TypeScript现在可以捕获上面的bug,这对某些用户来说可能是一个破坏性改动,但却是非常帮助的。

弱类型(Weak Type)探测

TypeScript 2.4引入了“弱类型”的概念。 任何只包含了可选属性的类型被当作是“weak”。 比如,下面的Options类型是弱类型:

interface Options {
    data?: string,
    timeout?: number,
    maxRetries?: number,
}

在TypeScript 2.4里给弱类型赋值时,如果这个值的属性与弱类型的属性没有任何重叠属性时会得到一个错误。 比如:

function sendMessage(options: Options) {
    // ...
}

const opts = {
    payload: "hello world!",
    retryOnFail: true,
}

// 错误!
sendMessage(opts);
// 'opts' 和 'Options' 没有重叠的属性
// 可能我们想要用'data'/'maxRetries'来代替'payload'/'retryOnFail'

因为这是一个破坏性改动,你可能想要知道一些解决方法:

  1. 确定属性存在时再声明
  2. 给弱类型增加索引签名(比如 [propName: string]: {}
  3. 使用类型断言(比如opts as Options

TypeScript 2.3

ES5/ES3 的生成器和迭代支持

首先是一些 ES2016 的术语:

迭代器

ES2015引入了Iterator(迭代器),它表示提供了 next,return,以及 throw 三个方法的对象,具体满足以下接口:

interface Iterator<T> {
  next(value?: any): IteratorResult<T>;
  return?(value?: any): IteratorResult<T>;
  throw?(e?: any): IteratorResult<T>;
}

这种迭代器对于迭代可用的值时很有用,比如数组的元素或者Map的键。如果一个对象有一个返回Iterator对象的Symbol.iterator方法,那么我们说这个对象是“可迭代的”。

迭代器协议还定义了一些ES2015中的特性像for..of和展开运算符以及解构赋值中的数组的剩余运算的操作对象。

生成器

ES2015也引入了"生成器",生成器是可以通过Iterator接口和yield关键字被用来生成部分运算结果的函数。生成器也可以在内部通过yield*代理对与其他可迭代对象的调用。举例来说:

function* f() {
  yield 1;
  yield* [2, 3];
}

新的--downlevelIteration编译选项

之前迭代器只在编译目标为 ES6/ES2015 或者更新版本时可用。此外,设计迭代器协议的结构,比如for..of,如果编译目标低于ES6/ES2015,则只能在操作数组时被支持。

TypeScript 2.3 在 ES3 和 ES5 为编译目标时由--downlevelIteration编译选项增加了完整的对生成器和迭代器协议的支持。

通过--downlevelIteration编译选项,编译器会使用新的类型检查和输出行为,尝试调用被迭代对象的[Symbol.iterator]()方法 (如果有),或者在对象上创建一个语义上的数组迭代器。

注意这需要非数组的值有原生的Symbol.iterator或者Symbol.iterator的运行时模拟实现。

使用--downlevelIteration时,在 ES5/ES3 中for..of语句、数组解构、数组中的元素展开、函数调用、new 表达式在支持Symbol.iterator时可用,但即便没有定义Symbol.iterator,它们在运行时或开发时都可以被使用到数组上.

异步迭代

TypeScript 2.3 添加了对异步迭代器和生成器的支持,描述见当前的TC39 提案

异步迭代器

异步迭代引入了AsyncIterator,它和Iterator相似。实际上的区别在于AsyncIteratornextreturnthrow方法的返回的是迭代结果的Promise,而不是结果本身。这允许AsyncIterator在生成值之前的时间点就加入异步通知。AsyncIterator的接口如下:

interface AsyncIterator<T> {
  next(value?: any): Promise<IteratorResult<T>>;
  return?(value?: any): Promise<IteratorResult<T>>;
  throw?(e?: any): Promise<IteratorResult<T>>;
}

一个支持异步迭代的对象如果有一个返回AsyncIterator对象的Symbol.asyncIterator方法,被称作是“可迭代的”。

异步生成器

异步迭代提案引入了“异步生成器”,也就是可以用来生成部分计算结果的异步函数。异步生成器也可以通过yield*代理对可迭代对象或异步可迭代对象的调用:

async function* g() {
  yield 1;
  await sleep(100);
  yield* [2, 3];
  yield* (async function *() {
    await sleep(100);
    yield 4;
  })();
}

和生成器一样,异步生成器只能是函数声明,函数表达式,或者类或对象字面量的方法。箭头函数不能作为异步生成器。异步生成器除了一个可用的Symbol.asyncIterator引用外 (原生或三方实现),还需要一个可用的全局Promise实现(既可以是原生的,也可以是ES2015兼容的实现)。

for-await-of语句

最后,ES2015引入了for..of语句来迭代可迭代对象。相似的,异步迭代提案引入了for..await..of语句来迭代可异步迭代的对象。

async function f() {
  for await (const x of g()) {
     console.log(x);
  }
}

for..await..of语句仅在异步函数或异步生成器中可用。

注意事项

  • 始终记住我们对于异步迭代器的支持是建立在运行时有Symbol.asyncIterator支持的基础上的。你可能需要Symbol.asyncIterator的三方实现,虽然对于简单的目的可以仅仅是:(Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol.for("Symbol.asyncIterator");
  • 如果你没有声明AsyncIterator,还需要在--lib选项中加入esnext来获取AsyncIterator声明。
  • 最后, 如果你的编译目标是ES5或ES3,你还需要设置--downlevelIterators编译选项。

泛型参数默认类型

TypeScript 2.3 增加了对声明泛型参数默认类型的支持。

示例

考虑一个会创建新的HTMLElement的函数,调用时不加参数会生成一个Div,你也可以选择性地传入子元素的列表。之前你必须这么去定义:

declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;
declare function create<T extends HTMLElement, U extends HTMLElement>(element: T, children: U[]): Container<T, U[]>;

有了泛型参数默认类型,我们可以将定义化简为:

declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(element?: T, children?: U): Container<T, U>;

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的。
  • 必选的类型参数不能在可选的类型参数后。
  • 如果类型参数有约束,类型参数的默认类型必须满足这个约束。
  • 当指定类型实参时,你只需要指定必选类型参数的类型实参。 未指定的类型参数会被解析为它们的默认类型。
  • 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。
  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。
  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型。

新的--strict主要编译选项

TypeScript加入的新检查项为了避免不兼容现有项目通常都是默认关闭的。虽然避免不兼容是好事,但这个策略的一个弊端则是使配置最高类型安全越来越复杂,这么做每次TypeScript版本发布时都需要显示地加入新选项。有了--strict编译选项,就可以选择最高级别的类型安全(了解随着更新版本的编译器增加了增强的类型检查特性可能会报新的错误)。

新的--strict编译器选项包含了一些建议配置的类型检查选项。具体来说,指定--strict相当于是指定了以下所有选项(未来还可能包括更多选项):

  • --strictNullChecks
  • --noImplicitAny
  • --noImplicitThis
  • --alwaysStrict

确切地说,--strict编译选项会为以上列出的编译器选项设置默认值。这意味着还可以单独控制这些选项。比如:

--strict --noImplicitThis false

这将是开启除--noImplicitThis编译选项以外的所有严格检查选项。使用这个方式可以表述除某些明确列出的项以外的所有严格检查项。换句话说,现在可以在默认最高级别的类型安全下排除部分检查。

从TypeScript 2.3开始,tsc --init生成的默认tsconfig.json"compilerOptions"中包含了"strict: true"设置。这样一来,用tsc --init创建的新项目默认会开启最高级别的类型安全。

改进的--init输出

除了默认的--strict设置外,tsc --init还改进了输出。tsc --init默认生成的tsconfig.json文件现在包含了一些带描述的被注释掉的常用编译器选项. 你可以去掉相关选项的注释来获得期望的结果。我们希望新的输出能简化新项目的配置并且随着项目成长保持配置文件的可读性。

--checkJS选项下 .js 文件中的错误

即便使用了--allowJs,TypeScript编译器默认不会报 .js 文件中的任何错误。TypeScript 2.3 中使用--checkJs选项,.js文件中的类型检查错误也可以被报出.

你可以通过为它们添加// @ts-nocheck注释来跳过对某些文件的检查,反过来你也可以选择通过添加// @ts-check注释只检查一些.js文件而不需要设置--checkJs编译选项。你也可以通过添加// @ts-ignore到特定行的一行前来忽略这一行的错误.

.js文件仍然会被检查确保只有标准的 ECMAScript 特性,类型标注仅在.ts文件中被允许,在.js中会被标记为错误。JSDoc注释可以用来为你的JavaScript代码添加某些类型信息,更多关于支持的JSDoc结构的详情,请浏览JSDoc支持文档

有关详细信息,请浏览类型检查JavaScript文件文档

TypeScript 2.2

支持混合类

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

首先是一些术语

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

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

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

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

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

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

class Point {
    constructor(public x: number, public y: number) {}
}

class Person {
    constructor(public name: string) {}
}

type Constructor<T> = new(...args: any[]) => T;

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = "";
        }
    }
}

const TaggedPoint = Tagged(Point);

let point = new TaggedPoint(10, 20);
point._tag = "hello";

class Customer extends Tagged(Person) {
    accountBalance: number;
}

let customer = new Customer("Joe");
customer._tag = "test";
customer.accountBalance = 0;

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

interface Point {
    x: number;
    y: number;
}

const WithLocation = <T extends Constructor<Point>>(Base: T) =>
    class extends Base {
        getLocation(): [number, number] {
            return [this.x, this.y];
        }
    }

object类型

TypeScript没有表示非基本类型的类型,即不是number | string | boolean | symbol | null | undefined的类型。一个新的object类型登场。

使用object类型,可以更好地表示类似Object.create这样的API。例如:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

支持new.target

new.target元属性是ES2015引入的新语法。当通过new构造函数创建实例时,new.target的值被设置为对最初用于分配实例的构造函数的引用。如果一个函数不是通过new构造而是直接被调用,那么new.target的值被设置为undefined

当在类的构造函数中需要设置Object.setPrototypeOf__proto__时,new.target就派上用场了。在NodeJS v4及更高版本中继承Error类就是这样的使用案例。

示例

class CustomError extends Error {
    constructor(message?: string) {
        super(message); // 'Error' breaks prototype chain here
        Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
    }
}

生成JS代码:

var CustomError = (function (_super) {
  __extends(CustomError, _super);
  function CustomError() {
    var _newTarget = this.constructor;
    var _this = _super.apply(this, arguments);  // 'Error' breaks prototype chain here
    _this.__proto__ = _newTarget.prototype; // restore prototype chain
    return _this;
  }
  return CustomError;
})(Error);

new.target也适用于编写可构造的函数,例如:

function f() {
  if (new.target) { /* called via 'new' */ }
}

编译为:

function f() {
  var _newTarget = this && this instanceof f ? this.constructor : void 0;
  if (_newTarget) { /* called via 'new' */ }
}

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

TypeScript 2.2改进了对表达式中可空操作数的检查。具体来说,这些现在被标记为错误:

  • 如果+运算符的任何一个操作数是可空的,并且两个操作数都不是anystring类型。
  • 如果-***/<<>>>>>, &, |^运算符的任何一个操作数是可空的。
  • 如果<><=>=in运算符的任何一个操作数是可空的。
  • 如果instanceof运算符的右操作数是可空的。
  • 如果一元运算符+-~++或者--的操作数是可空的。

如果操作数的类型是nullundefined或者包含nullundefined的联合类型,则操作数视为可空的。注意:包含nullundefined的联合类型只会出现在--strictNullChecks模式中,因为常规类型检查模式下nullundefined在联合类型中是不存在的。

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

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

interface StringMap<T> {
    [x: string]: T;
}

const map: StringMap<number>;

map["prop1"] = 1;
map.prop2 = 2;

这仅适用于具有显式字符串索引签名的类型。在类型使用上使用.符号访问未知属性仍然是一个错误。

支持在JSX子元素上使用扩展运算符

TypeScript 2.2增加了对在JSX子元素上使用扩展运算符的支持。更多详情请看facebook/jsx#57

示例

function Todo(prop: { key: number, todo: string }) {
    return <div>{prop.key.toString() + prop.todo}</div>;
}

function TodoList({ todos }: TodoListProps) {
    return <div>
        {...todos.map(todo => <Todo key={todo.id} todo={todo.todo} />)}
    </div>;
}

let x: TodoListProps;

<TodoList {...x} />

新的jsx: react-native

React-native构建管道期望所有文件都具有.js扩展名,即使该文件包含JSX语法。新的--jsx编译参数值react-native将在输出文件中坚持JSX语法,但是给它一个.js扩展名。

TypeScript 2.1

keyof和查找类型

在JavaScript中属性名称作为参数的API是相当普遍的,但是到目前为止还没有表达在那些API中出现的类型关系。

输入索引类型查询或keyof,索引类型查询keyof T产生的类型是T的属性名称。keyof T的类型被认为是string的子类型。

示例

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

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

示例

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

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

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

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

Person的可选属性类型将是这样:

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

使用映射类型,PartialPerson可以写成是Person类型的广义变换:

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

type PartialPerson = Partial<Person>;

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

Partial外,映射类型可以表示许多有用的类型转换:

// 保持类型相同,但每个属性是只读的。
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 }
};

Partial,Readonly,RecordPick

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

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

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

我们还包括两个其他实用程序类型:RecordPick

// 从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 }
// 对于类型T的每个属性K,将其转换为U
function mapObject<K extends string | number, 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 }

对象扩展运算符和rest运算符

TypeScript 2.1带来了ESnext扩展运算符和rest运算符的支持。

类似于数组扩展,展开对象可以方便得到浅拷贝:

let copy = { ...original };

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

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

还可以重写现有属性并添加新属性.:

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

指定展开操作的顺序确定哪些属性在最终的结果对象中。相同的属性,后面的属性会“覆盖”前面的属性。

与对象扩展运算符相对的是对象rest运算符,因为它可以提取解构元素中剩余的元素:

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。这可能需要获取Promisepolyfill,或者依赖运行时的版本。我们还需要通过设置lib编译参数,比如"dom","es2015""dom","es2015.promise","es5"来确保TypeScript知道Promise可用。

示例

tsconfig.json

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

dramaticWelcome.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

npm install tslib

然后,使用--importHelpers编译你的文件:

tsc --module commonjs --importHelpers a.ts

因此下面的输入,生成的.js文件将包含tslib的导入和使用__assign辅助函数替代内联操作。

export const o = { a: 1, name: "o" };
export const copy = { ...o };
"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编译参数后仍将被标记为错误。

// Succeeds if `node_modules/asdf/index.js` exists
import { x } from "asdf";

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

TypeScript 2.1支持三个新的编译版本值--target ES2016,--target ES2017--target ESNext

使用target--target ES2016将指示编译器不要编译ES2016特有的特性,比如**操作符。

同样,--target ES2017将指示编译器不要编译ES2017特有的特性像async/await

--target ESNext则对应最新的ES提议特性支持.

改进any类型推断

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

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

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

使用TypeScript 2.1,TypeScript不是仅仅选择any类型,而是基于你后面的赋值来推断类型。

仅当设置了--noImplicitAny编译参数时,才会启用此选项。

示例

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向其中添加的元素来_不断改变_自身的类型。

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错误只会在编译器无法知道一个没有类型注解的变量的类型时才会报告。

示例

function f3() {
    let x = [];  // 错误:当变量'x'类型无法确定时,它隐式具有'any[]'类型。
    x.push(5);
    function g() {
        x;    // 错误:变量'x'隐式具有'any【】'类型。
    }
}

更好的字面量类型推断

字符串、数字和布尔字面量类型(如:"abc"1true)之前仅在存在显式类型注释时才被推断。从TypeScript 2.1开始,字面量类型_总是_推断为默认值。

不带类型注解的const变量或readonly属性的类型推断为字面量初始化的类型。已经初始化且不带类型注解的let变量、var变量、形参或非readonly属性的类型推断为初始值的扩展字面量类型。字符串字面量扩展类型是string,数字字面量扩展类型是number,truefalse的字面量类型是boolean,还有枚举字面量扩展类型是枚举。

示例

const c1 = 1;  // Type 1
const c2 = c1;  // Type 1
const c3 = "abc";  // Type "abc"
const c4 = true;  // Type true
const c5 = cond ? 1 : "abc";  // Type 1 | "abc"

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

字面量类型扩展可以通过显式类型注解来控制。具体来说,当为不带类型注解的const局部变量推断字面量类型的表达式时,var变量获得扩展字面量类型推断。但是当const局部变量有显式字面量类型注解时,var变量获得非扩展字面量类型。

示例

const c1 = "hello";  // Widening type "hello"
let v1 = c1;  // Type string

const c2: "hello" = "hello";  // Type "hello"
let v2 = c2;  // Type "hello"

将基类构造函数的返回值作为'this'

在ES2015中,构造函数的返回值(它是一个对象)隐式地将this的值替换为super()的任何调用者。因此,有必要捕获任何潜在的super()的返回值并替换为this。此更改允许使用自定义元素,利用此元素可以使用用户编写的构造函数初始化浏览器分配的元素。

示例

class Base {
    x: number;
    constructor() {
        // 返回一个除“this”之外的新对象
        return {
            x: 1,
        };
    }
}

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

生成:

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

这在继承内置类如ErrorArrayMap等的行为上有了破坏性的改变。请阅读extending built-ins breaking change documentation

配置继承

通常一个项目有多个输出版本,比如ES5ES2015,调试和生产或CommonjsSystem。只有几个配置选项在这两个版本之间改变,并且维护多个tsconfig.json文件是麻烦的。

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

  • extendstsconfig.json是新的顶级属性(与compilerOptionsfilesincludeexclude一起)。
  • extends的值是包含继承自其它tsconfig.json路径的字符串。
  • 首先加载基本文件中的配置,然后由继承配置文件重写。
  • 如果遇到循环,我们报告错误。
  • 继承配置文件中的filesincludeexclude会重写基本配置文件中相应的值。
  • 在配置文件中找到的所有相对路径将相对于它们来源的配置文件来解析。

示例

configs/base.json:

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

configs/tests.json:

{
  "compilerOptions": {
    "preserveConstEnums": true,
    "stripComments": false,
    "sourceMaps": true
  },
  "exclude": [
    "../tests/baselines",
    "../tests/scenarios"
  ],
  "include": [
    "../tests/**/*.ts"
  ]
}

tsconfig.json:

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

tsconfig.nostrictnull.json:

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

新编译参数--alwaysStrict

使用--alwaysStrict调用编译器原因:1.在严格模式下解析的所有代码。2.在每一个生成文件上输出"use strict";指令;

模块会自动使用严格模式解析。对于非模块代码,建议使用该编译参数。

TypeScript 2.0

Null和undefined类型

TypeScript现在有两个特殊的类型:Null和Undefined, 它们的值分别是nullundefined。 以前这是不可能明确地命名这些类型的,但是现在nullundefined不管在什么类型检查模式下都可以作为类型名称使用。

以前类型检查器认为nullundefined赋值给一切。实际上,nullundefined是每一个类型的有效值, 并且不能明确排除它们(因此不可能检测到错误)。

--strictNullChecks

--strictNullChecks可以切换到新的严格空检查模式中。

在严格空检查模式中,nullundefined值_不再_属于任何类型的值,仅仅属于它们自己类型和any类型的值 (还有一个例外,undefined也能赋值给void)。因此,尽管在常规类型检查模式下TT | undefined被认为是相同的 (因为undefined被认为是任何T的子类型),但是在严格类型检查模式下它们是不同的, 并且仅仅T | undefined允许有undefined值,TT | null的关系同样如此。

示例

// 使用--strictNullChecks参数进行编译的
let x: number;
let y: number | undefined;
let z: number | null | undefined;
x = 1;  // 正确
y = 1;  // 正确
z = 1;  // 正确
x = undefined;  // 错误
y = undefined;  // 正确
z = undefined;  // 正确
x = null;  // 错误
y = null;  // 错误
z = null;  // 正确
x = y;  // 错误
x = z;  // 错误
y = x;  // 正确
y = z;  // 错误
z = x;  // 正确
z = y;  // 正确

使用前赋值检查

在严格空检查模式中,编译器要求未包含undefined类型的局部变量在使用之前必须先赋值。

示例

// 使用--strictNullChecks参数进行编译
let x: number;
let y: number | null;
let z: number | undefined;
x;  // 错误,使用前未赋值
y;  // 错误,使用前未赋值
z;  // 正确
x = 1;
y = null;
x;  // 正确
y;  // 正确

编译器通过执行_基于控制流的类型分析_检查变量明确被赋过值。在本篇文章后面会有进一步的细节。

可选参数和属性

可选参数和属性会自动把undefined添加到他们的类型中,即使他们的类型注解明确不包含undefined。例如,下面两个类型是完全相同的:

// 使用--strictNullChecks参数进行编译
type T1 = (x?: number) => string;              // x的类型是 number | undefined
type T2 = (x?: number | undefined) => string;  // x的类型是 number | undefined

非null和非undefined类型保护

如果对象或者函数的类型包含nullundefined,那么访问属性或调用函数时就会产生编译错误。因此,对类型保护进行了扩展,以支持对非null和非undefined的检查。

示例

// 使用--strictNullChecks参数进行编译
declare function f(x: number): string;
let x: number | null | undefined;
if (x) {
    f(x);  // 正确,这里的x类型是number
}
else {
    f(x);  // 错误,这里的x类型是number?
}
let a = x != null ? f(x) : "";  // a的类型是string
let b = x && f(x);  // b的类型是 string | 0 | null | undefined

非null和非undefined类型保护可以使用==!====!==操作符和nullundefined进行比较,如x != nullx === undefined。对被试变量类型的影响准确地反映了JavaScript的语义(比如,双等号运算符检查两个值无论你指定的是null还是undefined,然而三等于号运算符仅仅检查指定的那一个值)。

类型保护中的点名称

类型保护以前仅仅支持对局部变量和参数的检查。现在类型保护支持检查由变量或参数名称后跟一个或多个访问属性组成的“点名称”。

示例

interface Options {
    location?: {
        x?: number;
        y?: number;
    };
}

function foo(options?: Options) {
    if (options && options.location && options.location.x) {
        const x = options.location.x;  // x的类型是number
    }
}

点名称的类型保护和用户定义的类型保护函数,还有typeofinstanceof操作符一起工作,并且不依赖--strictNullChecks编译参数。

对点名称进行类型保护后给点名称任一部分赋值都会导致类型保护无效。例如,对x.y.z进行了类型保护后给xx.yx.y.z赋值,都会导致x.y.z类型保护无效。

表达式操作符

表达式操作符允许运算对象的类型包含null和/或undefined,但是总是产生非null和非undefined类型的结果值。

// 使用--strictNullChecks参数进行编译
function sum(a: number | null, b: number | null) {
    return a + b;  // 计算的结果值类型是number
}

&&操作符添加null和/或undefined到右边操作对象的类型中取决于当前左边操作对象的类型,||操作符从左边联合类型的操作对象的类型中将nullundefined同时删除。

// 使用--strictNullChecks参数进行编译
interface Entity {
    name: string;
}
let x: Entity | null;
let s = x && x.name;  // s的类型是string | null
let y = x || { name: "test" };  // y的类型是Entity

类型扩展

在严格空检查模式中,nullundefined类型是_不会_扩展到any类型中的。

let z = null;  // z的类型是null

在常规类型检查模式中,由于扩展,会推断z的类型是any,但是在严格空检查模式中,推断znull类型(因此,如果没有类型注释,nullz的唯一值)。

非空断言操作符

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符!可以用于断言操作对象是非null和非undefined类型的。具体而言,运算x!产生一个不包含nullundefinedx的值。断言的形式类似于<T>xx as T!非空断言操作符会从编译成的JavaScript代码中移除。

// 使用--strictNullChecks参数进行编译
function validateEntity(e?: Entity) {
    // 如果e是null或者无效的实体,就会抛出异常
}

function processEntity(e?: Entity) {
    validateEntity(e);
    let s = e!.name;  // 断言e是非空并访问name属性
}

兼容性

这些新特性是经过设计的,使得它们能够在严格空检查模式和常规类型检查模式下都能够使用。尤其是在常规类型检查模式中,nullundefined类型会自动从联合类型中删除(因为它们是其它所有类型的子类型),!非空断言表达式操作符也被允许使用但是没有任何作用。因此,声明文件使用null和undefined敏感类型更新后,在常规类型模式中仍然是可以向后兼容使用的。

在实际应用中,严格空检查模式要求编译的所有文件都是null和undefined敏感类型。

基于控制流的类型分析

TypeScript 2.0实现了对局部变量和参数的控制流类型分析。以前,对类型保护进行类型分析仅限于if语句和?:条件表达式,并且不包括赋值和控制流结构的影响,例如returnbreak语句。使用TypeScript 2.0,类型检查器会分析语句和表达式所有可能的控制流,在任何指定的位置对声明为联合类型的局部变量或参数产生最可能的具体类型(缩小范围的类型)。

示例

function foo(x: string | number | boolean) {
    if (typeof x === "string") {
        x; // 这里x的类型是string
        x = 1;
        x; // 这里x的类型是number
    }
    x; // 这里x的类型是number | boolean
}

function bar(x: string | number) {
    if (typeof x === "number") {
        return;
    }
    x; // 这里x的类型是string
}

基于控制流的类型分析在--strictNullChecks模式中尤为重要,因为可空类型使用联合类型来表示:

function test(x: string | null) {
    if (x === null) {
        return;
    }
    x; // 在函数的剩余部分中,x类型是string
}

而且,在--strictNullChecks模式中,基于控制流的分析包括,对类型不允许为undefined的局部变量有_明确赋值_的分析。

function mumble(check: boolean) {
    let x: number; // 类型不允许为undefined
    x; // 错误,x是undefined
    if (check) {
        x = 1;
        x; // 正确
    }
    x; // 错误,x可能是undefi
    x = 2;
    x; // 正确
}

标记联合类型

TypeScript 2.0实现了标记(或区分)联合类型。具体而言,TS编译器现在支持类型保护,基于判别属性的检查来缩小联合类型的范围,并且switch语句也支持此特性。

示例

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    // 在下面的switch语句中,s的类型在每一个case中都被缩小
    // 根据判别属性的值,变量的其它属性不使用类型断言就可以被访问
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
    }
}

function test1(s: Shape) {
    if (s.kind === "square") {
        s;  // Square
    }
    else {
        s;  // Rectangle | Circle
    }
}

function test2(s: Shape) {
    if (s.kind === "square" || s.kind === "rectangle") {
        return;
    }
    s;  // Circle
}

_判别属性类型保护_是x.p == vx.p === vx.p != v或者x.p !== v其中的一种表达式,pv是一个属性和字符串字面量类型或字符串字面量联合类型的表达式。判别属性类型保护缩小x的类型到由判别属性pv的可能值之一组成的类型。

请注意,我们目前只支持字符串字面值类型的判别属性。我们打算以后添加对布尔值和数字字面量类型的支持。

never类型

TypeScript 2.0引入了一个新原始类型nevernever类型表示值的类型从不出现。具体而言,never是永不返回函数的返回类型,也是变量在类型保护中永不为true的类型。

never类型具有以下特征:

  • never是所有类型的子类型并且可以赋值给所有类型。
  • 没有类型是never的子类型或能赋值给nevernever类型本身除外)。
  • 在函数表达式或箭头函数没有返回类型注解时,如果函数没有return语句,或者只有never类型表达式的return语句,并且如果函数是不可执行到终点的(例如通过控制流分析决定的),则推断函数的返回类型是never
  • 在有明确never返回类型注解的函数中,所有return语句(如果有的话)必须有never类型的表达式并且函数的终点必须是不可执行的。

因为never是每一个类型的子类型,所以它总是在联合类型中被省略,并且在函数中只要其它类型被返回,类型推断就会忽略never类型。

一些返回never函数的示例:

// 函数返回never必须无法执行到终点
function error(message: string): never {
    throw new Error(message);
}

// 推断返回类型是never
function fail() {
    return error("Something failed");
}

// 函数返回never必须无法执行到终点
function infiniteLoop(): never {
    while (true) {
    }
}

一些函数返回never的使用示例:

// 推断返回类型是number
function move1(direction: "up" | "down") {
    switch (direction) {
        case "up":
            return 1;
        case "down":
            return -1;
    }
    return error("Should never get here");
}

// 推断返回类型是number
function move2(direction: "up" | "down") {
    return direction === "up" ? 1 :
        direction === "down" ? -1 :
        error("Should never get here");
}

// 推断返回类型是T
function check<T>(x: T | undefined) {
    return x || error("Undefined value");
}

因为never可以赋值给每一个类型,当需要回调函数返回一个更加具体的类型时,函数返回never类型可以用于检测返回类型是否正确:

function test(cb: () => string) {
    let s = cb();
    return s;
}

test(() => "hello");
test(() => fail());
test(() => { throw new Error(); })

只读属性和索引签名

属性或索引签名现在可以使用readonly修饰符声明为只读的。

只读属性可以初始化和在同一个类的构造函数中被赋值,但是在其它情况下对只读属性的赋值是不允许的。

此外,有几种情况下实体_隐式_只读的:

  • 属性声明只使用get访问器而没有使用set访问器被视为只读的。
  • 在枚举类型中,枚举成员被视为只读属性。
  • 在模块类型中,导出的const变量被视为只读属性。
  • import语句中声明的实体被视为只读的。
  • 通过ES2015命名空间导入访问的实体被视为只读的(例如,当foo当作import * as foo from "foo"声明时,foo.x是只读的)。

示例

interface Point {
    readonly x: number;
    readonly y: number;
}

var p1: Point = { x: 10, y: 20 };
p1.x = 5;  // 错误,p1.x是只读的

var p2 = { x: 1, y: 1 };
var p3: Point = p2;  // 正确,p2的只读别名
p3.x = 5;  // 错误,p3.x是只读的
p2.x = 5;  // 正确,但是因为别名使用,同时也改变了p3.x
class Foo {
    readonly a = 1;
    readonly b: string;
    constructor() {
        this.b = "hello";  // 在构造函数中允许赋值
    }
}
let a: Array<number> = [0, 1, 2, 3, 4];
let b: ReadonlyArray<number> = a;
b[5] = 5;      // 错误,元素是只读的
b.push(5);     // 错误,没有push方法(因为这会修改数组)
b.length = 3;  // 错误,length是只读的
a = b;         // 错误,缺少修改数组的方法

指定函数中this类型

紧跟着类和接口,现在函数和方法也可以声明this的类型了。

函数中this的默认类型是any。从TypeScript 2.0开始,你可以提供一个明确的this参数。this参数是伪参数,它位于函数参数列表的第一位:

function f(this: void) {
    // 确保`this`在这个独立的函数中无法使用
}

回调函数中的this参数

库也可以使用this参数声明回调函数如何被调用。

示例

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

this:void意味着addClickListener预计onclick是一个this参数不需要类型的函数。

现在如果你在调用代码中对this进行了类型注释:

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // 哎哟,在这里使用this.在运行中使用这个回调函数将会崩溃。
        this.info = e.message;
    };
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // 错误!

--noImplicitThis

TypeScript 2.0还增加了一个新的编译选项用来标记函数中所有没有明确类型注释的this的使用。

tsconfig.json支持文件通配符

文件通配符来啦!!支持文件通配符一直是最需要的特性之一

类似文件通配符的文件模式支持两个属性"include""exclude"

示例

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "outFile": "../../built/local/tsc.js",
        "sourceMap": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}

支持文件通配符的符号有:

  • *匹配零个或多个字符(不包括目录)
  • ?匹配任意一个字符(不包括目录)
  • **/递归匹配所有子目录

如果文件通配符模式语句中只包含*.*,那么只匹配带有扩展名的文件(例如默认是.ts.tsx.d.ts,如果allowJs设置为true.js.jsx也属于默认)。

如果"files""include"都没有指定,编译器默认包含所有目录中的TypeScript文件(.ts.d.ts.tsx),除了那些使用exclude属性排除的文件外。如果allowJs设置为true,JS文件(.js.jsx)也会被包含进去。

如果"files""include"都指定了,编译器将包含这两个属性指定文件的并集。使用ourDir编译选项指定的目录文件总是被排除,即使"exclude"属性指定的文件也会被删除,但是files属性指定的文件不会排除。

"exclude"属性指定的文件会对"include"属性指定的文件过滤。但是对"files"指定的文件没有任何作用。当没有明确指定时,"exclude"属性默认会排除node_modulesbower_componentsjspm_packages目录。

模块解析增加:BaseUrl、路径映射、rootDirs和追踪

TypeScript 2.0提供了一系列额外的模块解析属性告诉编译器去哪里可以找到给定模块的声明。

更多详情,请参阅模块解析文档。

Base URL

使用了AMD模块加载器并且模块在运行时”部署“到单文件夹的应用程序中使用baseUrl是一种常用的做法。所有非相对名称的模块导入被认为是相对于baseUrl的。

示例

{
  "compilerOptions": {
    "baseUrl": "./modules"
  }
}

现在导入moduleA将会在./modules/moduleA中查找。

import A from "moduleA";

路径映射

有时模块没有直接位于_baseUrl_中。加载器使用映射配置在运行时去映射模块名称和文件,请参阅RequireJs文档SystemJS文档

TypeScript编译器支持tsconfig文件中使用"paths"属性映射的声明。

示例

例如,导入"jquery"模块在运行时会被转换为"node_modules/jquery/dist/jquery.slim.min.js"

{
    "compilerOptions": {
        "baseUrl": "./node_modules",
        "paths": {
        "jquery": ["jquery/dist/jquery.slim.min"]
        }
    }
}

使用"paths"也允许更复杂的映射,包括多次后退的位置。考虑一个只有一个地方的模块是可用的,其它的模块都在另一个地方的项目配置。

rootDirs和虚拟目录

使用rootDirs,你可以告知编译器的_根目录_组合这些“虚拟”目录。因此编译器在这些“虚拟”目录中解析相对导入模块,仿佛是合并到一个目录中一样。

示例

给定的项目结构

 src
 └── views
     └── view1.ts (imports './template1')
     └── view2.ts

 generated
 └── templates
         └── views
             └── template1.ts (imports './view2')

构建步骤将复制/src/views/generated/templates/views目录下的文件输出到同一个目录中。在运行时,视图期望它的模板和它存在同一目录中,因此应该使用相对名称"./template"导入。

"rootDir"指定的一组根目录的内容将会在运行时合并。因此在我们的例子,tsconfig.json文件应该类似于:

{
  "compilerOptions": {
    "rootDirs": [
      "src/views",
      "generated/templates/views"
    ]
  }
}

追踪模块解析

--traceResolution提供了一种方便的方法,以了解模块如何被编译器解析的。

tsc --traceResolution

快捷外部模块声明

当你使用一个新模块时,如果不想要花费时间书写一个声明时,现在你可以使用快捷声明以便以快速开始。

declarations.d.ts

declare module "hot-new-module";

所有从快捷模块的导入都具有任意类型。

import x, {y} from "hot-new-module";
x(y);

模块名称中的通配符

以前使用模块加载器(例如AMDSystemJS)导入没有代码的资源是不容易的。之前,必须为每个资源定义一个外部模块声明。

TypeScript 2.0支持使用通配符符号(*)定义一类模块名称。这种方式,一个声明只需要一次扩展名,而不再是每一个资源。

示例

declare module "*!text" {
    const content: string;
    export default content;
}
// Some do it the other way around.
declare module "json!*" {
    const value: any;
    export default value;
}

现在你可以导入匹配"*!text""json!*"的东西了。

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

当从一个基于非类型化的代码迁移时,通配符模块的名称可能更加有用。结合快捷外部模块声明,一组模块可以很容易地声明为any

示例

declare module "myLibrary/*";

所有位于myLibrary目录之下的模块的导入都被编译器认为是any类型,因此这些模块的任何类型检查都会被关闭。

import { readFile } from "myLibrary/fileSystem/readFile`;

readFile(); // readFile是'any'类型

支持UMD模块定义

一些库被设计为可以使用多种模块加载器或者不是使用模块加载器(全局变量)来使用,这被称为UMD同构模块。这些库可以通过导入或全局变量访问。

举例:

math-lib.d.ts

export const isPrime(x: number): boolean;
export as namespace mathLib;

然后,该库可作为模块导入使用:

import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // 错误:无法在模块内部使用全局定义

它也可以被用来作为一个全局变量,只限于没有importexport脚本文件中。

mathLib.isPrime(2);

可选类属性

现在可以在类中声明可选属性和方法,与接口类似。

示例

class Bar {
    a: number;
    b?: number;
    f() {
        return 1;
    }
    g?(): number;  // 可选方法的方法体可以省略
    h?() {
        return 2;
    }
}

--strictNullChecks模式下编译时,可选属性和方法会自动添加undefined到它们的类型中。因此,上面的b属性类型是number | undefined,上面g方法的类型是(()=> number) | undefined。使用类型保护可以去除undefined

私有的和受保护的构造函数

类的构造函数可以被标记为privateprotected。私有构造函数的类不能在类的外部实例化,并且也不能被继承。受保护构造函数的类不能再类的外部实例化,但是可以被继承。

示例

class Singleton {
    private static instance: Singleton;

    private constructor() { }

    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

let e = new Singleton(); // 错误:Singleton的构造函数是私有的。
let v = Singleton.getInstance();

抽象属性和访问器

抽象类可以声明抽象属性和、或访问器。所有子类将需要声明抽象属性或者被标记为抽象的。抽象属性不能初始化。抽象访问器不能有具体代码块。

示例

abstract class Base {
    abstract name: string;
    abstract get value();
    abstract set value(v: number);
}

class Derived extends Base {
    name = "derived";

    value = 1;
}

隐式索引签名

如果对象字面量中所有已知的属性是赋值给索引签名,那么现在对象字面量类型可以赋值给索引签名类型。这使得一个使用对象字面量初始化的变量作为参数传递给期望参数是map或dictionary的函数成为可能:

function httpService(path: string, headers: { [x: string]: string }) { }

const headers = {
    "Content-Type": "application/x-www-form-urlencoded"
};

httpService("", { "Content-Type": "application/x-www-form-urlencoded" });  // 可以
httpService("", headers);  // 现在可以,以前不可以。

使用--lib编译参数包含内置类型声明

获取ES6/ES2015内置API声明仅限于target: ES6。输入--lib,你可以使用--lib指定一组项目所需要的内置API。比如说,如果你希望项目运行时支持MapSetPromise(例如现在静默更新浏览器),直接写--lib es2015.collection,es2015.promise就好了。同样,你也可以排除项目中不需要的声明,例如在node项目中使用--lib es5,es6排除DOM。

下面是列出了可用的API:

  • dom
  • webworker
  • es5
  • es6 / es2015
  • es2015.core
  • es2015.collection
  • es2015.iterable
  • es2015.promise
  • es2015.proxy
  • es2015.reflect
  • es2015.generator
  • es2015.symbol
  • es2015.symbol.wellknown
  • es2016
  • es2016.array.include
  • es2017
  • es2017.object
  • es2017.sharedmemory
  • scripthost

示例

tsc --target es5 --lib es5,es2015.promise
"compilerOptions": {
    "lib": ["es5", "es2015.promise"]
}

使用--noUnusedParameters--noUnusedLocals标记未使用的声明

TypeScript 2.0有两个新的编译参数来帮助你保持一个干净的代码库。-noUnusedParameters编译参数标记所有未使用的函数或方法的参数错误。--noUnusedLocals标记所有未使用的局部(未导出)声明像变量、函数、类和导入等等,另外未使用的私有类成员在--noUnusedLocals作用下也会标记为错误。

示例

import B, { readFile } from "./b";
//     ^ 错误:`B`声明了,但是没有使用。
readFile();


export function write(message: string, args: string[]) {
    //                                 ^^^^  错误:'arg'声明了,但是没有使用。
    console.log(message);
}

使用以_开头命名的参数声明不会被未使用参数检查。例如:

function returnNull(_a) { // 正确
    return null;
}

模块名称允许.js扩展名

TypeScript 2.0之前,模块名称总是被认为是没有扩展名的。例如,导入一个模块import d from "./moduleA.js",则编译器在./moduleA.js.ts./moduleA.js.d.ts中查找"moduleA.js"的定义。这使得像SystemJS这种期望模块名称是URI的打包或加载工具很难使用。

使用TypeScript 2.0,编译器将在./moduleA.ts./moduleA.d.ts中查找"moduleA.js"的定义。

支持编译参数target : es5module: es6同时使用

之前编译参数target : es5module: es6同时使用被认为是无效的,但是现在是有效的。这将有助于使用基于ES2015的tree-shaking(将无用代码移除)比如rollup

函数形参和实参列表末尾支持逗号

现在函数形参和实参列表末尾允许有逗号。这是对第三阶段的ECMAScript提案的实现, 并且会编译为可用的 ES3/ES5/ES6。

示例

function foo(
  bar: Bar,
  baz: Baz, // 形参列表末尾添加逗号是没有问题的。
) {
  // 具体实现……
}

foo(
  bar,
  baz, // 实参列表末尾添加逗号同样没有问题
);

新编译参数--skipLibCheck

TypeScript 2.0添加了一个新的编译参数--skipLibCheck,该参数可以跳过声明文件(以.d.ts为扩展名的文件)的类型检查。当一个程序包含有大量的声明文件时,编译器需要花费大量时间对已知不包含错误的声明进行类型检查,通过跳过声明文件的类型检查,编译时间可能会大大缩短。

由于一个文件中的声明可以影响其他文件中的类型检查,当指定--skipLibCheck时,一些错误可能检测不到。比如说, 如果一个非声明文件中的类型被声明文件用到, 可能仅在声明文件被检查时能发现错误. 不过这种情况在实际使用中并不常见。

允许在声明中重复标识符

这是重复定义错误的一个常见来源。多个声明文件定义相同的接口成员。

TypeScript 2.0放宽了这一约束,并允许可以不同代码块中出现重复的标识符, 只要它们有_完全相同_的类型。

在同一代码块重复定义仍不允许。

示例

interface Error {
    stack?: string;
}


interface Error {
    code?: string;
    path?: string;
    stack?: string;  // OK
}

新编译参数--declarationDir

--declarationDir可以使生成的声明文件和JavaScript文件不在同一个位置中。

TypeScript 1.8

类型参数约束

在 TypeScript 1.8 中, 类型参数的限制可以引用自同一个类型参数列表中的类型参数. 在此之前这种做法会报错. 这种特性通常被叫做 F-Bounded Polymorphism.

例子

function assign<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
assign(x, { b: 10, d: 20 });
assign(x, { e: 0 });  // 错误

控制流错误分析

TypeScript 1.8 中引入了控制流分析来捕获开发者通常会遇到的一些错误.

详情见接下来的内容, 可以上手尝试:

cfa

不可及的代码

一定无法在运行时被执行的语句现在会被标记上代码不可及错误. 举个例子, 在无条件限制的 return, throw, break 或者 continue 后的语句被认为是不可及的. 使用 --allowUnreachableCode 来禁用不可及代码的检测和报错.

例子

这里是一个简单的不可及错误的例子:

function f(x) {
    if (x) {
       return true;
    }
    else {
       return false;
    }

    x = 0; // 错误: 检测到不可及的代码.
}

这个特性能捕获的一个更常见的错误是在 return 语句后添加换行:

function f() {
    return            // 换行导致自动插入的分号
    {
        x: "string"   // 错误: 检测到不可及的代码.
    }
}

因为 JavaScript 会自动在行末结束 return 语句, 下面的对象字面量变成了一个代码块.

未使用的标签

未使用的标签也会被标记. 和不可及代码检查一样, 被使用的标签检查也是默认开启的. 使用 --allowUnusedLabels 来禁用未使用标签的报错.

例子

loop: while (x > 0) {  // 错误: 未使用的标签.
    x++;
}

隐式返回

JS 中没有返回值的代码分支会隐式地返回 undefined. 现在编译器可以将这种方式标记为隐式返回. 对于隐式返回的检查默认是被禁用的, 可以使用 --noImplicitReturns 来启用.

例子

function f(x) { // 错误: 不是所有分支都返回了值.
    if (x) {
        return false;
    }

    // 隐式返回了 `undefined`
}

Case 语句贯穿

TypeScript 现在可以在 switch 语句中出现贯穿的几个非空 case 时报错. 这个检测默认是关闭的, 可以使用 --noFallthroughCasesInSwitch 启用.

例子

switch (x % 2) {
    case 0: // 错误: switch 中出现了贯穿的 case.
        console.log("even");

    case 1:
        console.log("odd");
        break;
}

然而, 在下面的例子中, 由于贯穿的 case 是空的, 并不会报错:

switch (x % 3) {
    case 0:
    case 1:
        console.log("Acceptable");
        break;

    case 2:
        console.log("This is *two much*!");
        break;
}

React里的函数组件

TypeScript 现在支持函数组件. 它是可以组合其他组件的轻量级组件.

// 使用参数解构和默认值轻松地定义 'props' 的类型
const Greeter = ({name = 'world'}) => <div>Hello, {name}!</div>;

// 参数可以被检验
let example = <Greeter name='TypeScript 1.8' />;

如果需要使用这一特性及简化的 props, 请确认使用的是最新的 react.d.ts.

简化的 React props 类型管理

在 TypeScript 1.8 配合最新的 react.d.ts (见上方) 大幅简化了 props 的类型声明.

具体的:

  • 你不再需要显式的声明 refkey 或者 extend React.Props
  • refkey 属性会在所有组件上拥有正确的类型.
  • ref 属性在无状态函数组件上会被正确地禁用.

在模块中扩充全局或者模块作用域

用户现在可以为任何模块进行他们想要, 或者其他人已经对其作出的扩充. 模块扩充的形式和过去的包模块一致 (例如 declare module "foo" { } 这样的语法), 并且可以直接嵌在你自己的模块内, 或者在另外的顶级外部包模块中.

除此之外, TypeScript 还以 declare global { } 的形式提供了对于_全局_声明的扩充. 这能使模块对像 Array 这样的全局类型在必要的时候进行扩充.

模块扩充的名称解析规则与 importexport 声明中的一致. 扩充的模块声明合并方式与在同一个文件中声明是相同的.

不论是模块扩充还是全局声明扩充都不能向顶级作用域添加新的项目 - 它们只能为已经存在的声明添加 "补丁".

例子

这里的 map.ts 可以声明它会在内部修改在 observable.ts 中声明的 Observable 类型, 添加 map 方法.

// observable.ts
export class Observable<T> {
    // ...
}
// map.ts
import { Observable } from "./observable";

// 扩充 "./observable"
declare module "./observable" {

    // 使用接口合并扩充 'Observable' 类的定义
    interface Observable<T> {
        map<U>(proj: (el: T) => U): Observable<U>;
    }

}

Observable.prototype.map = /*...*/;
// consumer.ts
import { Observable } from "./observable";
import "./map";

let o: Observable<number>;
o.map(x => x.toFixed());

相似的, 在模块中全局作用域可以使用 declare global 声明被增强:

例子

// 确保当前文件被当做一个模块.
export {};

declare global {
    interface Array<T> {
        mapToNumbers(): number[];
    }
}

Array.prototype.mapToNumbers = function () { /* ... */ }

字符串字面量类型

接受一个特定字符串集合作为某个值的 API 并不少见. 举例来说, 考虑一个可以通过控制动画的渐变让元素在屏幕中滑动的 UI 库:

declare class UIElement {
    animate(options: AnimationOptions): void;
}

interface AnimationOptions {
    deltaX: number;
    deltaY: number;
    easing: string; // 可以是 "ease-in", "ease-out", "ease-in-out"
}

然而, 这容易产生错误 - 当用户错误不小心错误拼写了一个合法的值时, 并没有任何提示:

// 没有报错
new UIElement().animate({ deltaX: 100, deltaY: 100, easing: "ease-inout" });

在 TypeScript 1.8 中, 我们新增了字符串字面量类型. 这些类型和字符串字面量的写法一致, 只是写在类型的位置.

用户现在可以确保类型系统会捕获这样的错误. 这里是我们使用了字符串字面量类型的新的 AnimationOptions:

interface AnimationOptions {
    deltaX: number;
    deltaY: number;
    easing: "ease-in" | "ease-out" | "ease-in-out";
}

// 错误: 类型 '"ease-inout"' 不能复制给类型 '"ease-in" | "ease-out" | "ease-in-out"'
new UIElement().animate({ deltaX: 100, deltaY: 100, easing: "ease-inout" });

更好的联合/交叉类型接口

TypeScript 1.8 优化了源类型和目标类型都是联合或者交叉类型的情况下的类型推导. 举例来说, 当从 string | string[] 推导到 string | T 时, 我们将类型拆解为 string[]T, 这样就可以将 string[] 推导为 T.

例子

type Maybe<T> = T | void;

function isDefined<T>(x: Maybe<T>): x is T {
    return x !== undefined && x !== null;
}

function isUndefined<T>(x: Maybe<T>): x is void {
    return x === undefined || x === null;
}

function getOrElse<T>(x: Maybe<T>, defaultValue: T): T {
    return isDefined(x) ? x : defaultValue;
}

function test1(x: Maybe<string>) {
    let x1 = getOrElse(x, "Undefined");         // string
    let x2 = isDefined(x) ? x : "Undefined";    // string
    let x3 = isUndefined(x) ? "Undefined" : x;  // string
}

function test2(x: Maybe<number>) {
    let x1 = getOrElse(x, -1);         // number
    let x2 = isDefined(x) ? x : -1;    // number
    let x3 = isUndefined(x) ? -1 : x;  // number
}

使用 --outFile 合并 AMDSystem 模块

在使用 --module amd 或者 --module system 的同时制定 --outFile 将会把所有参与编译的模块合并为单个包括了多个模块闭包的输出文件.

每一个模块都会根据其相对于 rootDir 的位置被计算出自己的模块名称.

例子

// 文件 src/a.ts
import * as B from "./lib/b";
export function createA() {
    return B.createB();
}
// 文件 src/lib/b.ts
export function createB() {
    return { };
}

结果为:

define("lib/b", ["require", "exports"], function (require, exports) {
    "use strict";
    function createB() {
        return {};
    }
    exports.createB = createB;
});
define("a", ["require", "exports", "lib/b"], function (require, exports, B) {
    "use strict";
    function createA() {
        return B.createB();
    }
    exports.createA = createA;
});

支持 SystemJS 使用 default 导入

像 SystemJS 这样的模块加载器将 CommonJS 模块做了包装并暴露为 default ES6 导入项. 这使得在 SystemJS 和 CommonJS 的实现由于不同加载器不同的模块导出方式不能共享定义.

设置新的编译选项 --allowSyntheticDefaultImports 指明模块加载器会进行导入的 .ts.d.ts 中未指定的某种类型的默认导入项构建. 编译器会由此推断存在一个 default 导出项和整个模块自己一致.

此选项在 System 模块默认开启.

允许循环中被引用的 let/const

之前这样会报错, 现在由 TypeScript 1.8 支持. 循环中被函数引用的 let/const 声明现在会被输出为与 let/const 更新语义相符的代码.

例子

let list = [];
for (let i = 0; i < 5; i++) {
    list.push(() => i);
}

list.forEach(f => console.log(f()));

被编译为:

var list = [];
var _loop_1 = function(i) {
    list.push(function () { return i; });
};
for (var i = 0; i < 5; i++) {
    _loop_1(i);
}
list.forEach(function (f) { return console.log(f()); });

然后结果是:

0
1
2
3
4

改进的 for..in 语句检查

过去 for..in 变量的类型被推断为 any, 这使得编译器忽略了 for..in 语句内的一些不合法的使用.

从 TypeScript 1.8 开始:

  • for..in 语句中的变量隐含类型为 string.
  • 当一个有数字索引签名对应类型 T (比如一个数组) 的对象被一个 for..in 索引_有_数字索引签名并且_没有_字符串索引签名 (比如还是数组) 的对象的变量索引, 产生的值的类型为 T.

例子

var a: MyObject[];
for (var x in a) {   // x 的隐含类型为 string
    var obj = a[x];  // obj 的类型为 MyObject
}

模块现在输出时会加上 "use strict;"

对于 ES6 来说模块始终以严格模式被解析, 但这一点过去对于非 ES6 目标在生成的代码中并没有遵循. 从 TypeScript 1.8 开始, 输出的模块总会为严格模式. 由于多数严格模式下的错误也是 TS 编译时的错误, 多数代码并不会有可见的改动, 但是这也意味着有一些东西可能在运行时没有征兆地失败, 比如赋值给 NaN 现在会有运行时错误. 你可以参考这篇 MDN 上的文章 查看详细的严格模式与非严格模式的区别列表.

使用 --allowJs 加入 .js 文件

经常在项目中会有外部的非 TypeScript 编写的源文件. 一种方式是将 JS 代码转换为 TS 代码, 但这时又希望将所有 JS 代码和新的 TS 代码的输出一起打包为一个文件.

.js 文件现在允许作为 tsc 的输入文件. TypeScript 编译器会检查 .js 输入文件的语法错误, 并根据 --target--module 选项输出对应的代码. 输出也会和其他 .ts 文件一起. .js 文件的 source maps 也会像 .ts 文件一样被生成.

使用 --reactNamespace 自定义 JSX 工厂

在使用 --jsx react 的同时使用 --reactNamespace <JSX 工厂名称> 可以允许使用一个不同的 JSX 工厂代替默认的 React.

新的工厂名称会被用来调用 createElement__spread 方法.

例子

import {jsxFactory} from "jsxFactory";

var div = <div>Hello JSX!</div>

编译参数:

tsc --jsx react --reactNamespace jsxFactory --m commonJS

结果:

"use strict";
var jsxFactory_1 = require("jsxFactory");
var div = jsxFactory_1.jsxFactory.createElement("div", null, "Hello JSX!");

基于 this 的类型收窄

TypeScript 1.8 为类和接口方法扩展了用户定义的类型收窄函数.

this is T 现在是类或接口方法的合法的返回值类型标注. 当在类型收窄的位置使用时 (比如 if 语句), 函数调用表达式的目标对象的类型会被收窄为 T.

例子

class FileSystemObject {
    isFile(): this is File { return this instanceof File; }
    isDirectory(): this is Directory { return this instanceof Directory;}
    isNetworked(): this is (Networked & this) { return this.networked; }
    constructor(public path: string, private networked: boolean) {}
}

class File extends FileSystemObject {
    constructor(path: string, public content: string) { super(path, false); }
}
class Directory extends FileSystemObject {
    children: FileSystemObject[];
}
interface Networked {
    host: string;
}

let fso: FileSystemObject = new File("foo/bar.txt", "foo");
if (fso.isFile()) {
    fso.content; // fso 是 File
}
else if (fso.isDirectory()) {
    fso.children; // fso 是 Directory
}
else if (fso.isNetworked()) {
    fso.host; // fso 是 networked
}

官方的 TypeScript NuGet 包

从 TypeScript 1.8 开始, 将为 TypeScript 编译器 (tsc.exe) 和 MSBuild 整合 (Microsoft.TypeScript.targetsMicrosoft.TypeScript.Tasks.dll) 提供官方的 NuGet 包.

稳定版本可以在这里下载:

与此同时, 和每日npm包对应的每日 NuGet 包可以在https://myget.org下载:

tsc 错误信息更美观

我们理解大量单色的输出并不直观. 颜色可以帮助识别信息的始末, 这些视觉上的线索在处理复杂的错误信息时非常重要.

通过传递 --pretty 命令行选项, TypeScript 会给出更丰富的输出, 包含错误发生的上下文.

展示在 ConEmu 中美化之后的错误信息

高亮 VS 2015 中的 JSX 代码

在 TypeScript 1.8 中, JSX 标签现在可以在 Visual Studio 2015 中被分别和高亮.

jsx

通过 工具->选项->环境->字体与颜色 页面在 VB XML 颜色和字体设置中还可以进一步改变字体和颜色来自定义.

--project (-p) 选项现在接受任意文件路径

--project 命令行选项过去只接受包含了 tsconfig.json 文件的文件夹. 考虑到不同的构建场景, 应该允许 --project 指向任何兼容的 JSON 文件. 比如说, 一个用户可能会希望为 Node 5 编译 CommonJS 的 ES 2015, 为浏览器编译 AMD 的 ES5. 现在少了这项限制, 用户可以更容易地直接使用 tsc 管理不同的构建目标, 无需再通过一些奇怪的方式, 比如将多个 tsconfig.json 文件放在不同的目录中.

如果参数是一个路径, 行为保持不变 - 编译器会尝试在该目录下寻找名为 tsconfig.json 的文件.

允许 tsconfig.json 中的注释

为配置添加文档是很棒的! tsconfig.json 现在支持单行和多行注释.

{
    "compilerOptions": {
        "target": "ES2015", // 跑在 node v5 上, 呀!
        "sourceMap": true   // 让调试轻松一些
    },
    /*
     * 排除的文件
     */
    "exclude": [
        "file.d.ts"
    ]
}

支持输出到 IPC 驱动的文件

TypeScript 1.8 允许用户将 --outFile 参数和一些特殊的文件系统对象一起使用, 比如命名的管道 (pipe), 设备 (devices) 等.

举个例子, 在很多与 Unix 相似的系统上, 标准输出流可以通过文件 /dev/stdout 访问.

tsc foo.ts --outFile /dev/stdout

这一特性也允许输出给其他命令.

比如说, 我们可以输出生成的 JavaScript 给一个像 pretty-js 这样的格式美化工具:

tsc foo.ts --outFile /dev/stdout | pretty-js

改进了 Visual Studio 2015 中对 tsconfig.json 的支持

TypeScript 1.8 允许在任何种类的项目中使用 tsconfig.json 文件. 包括 ASP.NET v4 项目, 控制台应用, 以及 用 TypeScript 开发的 HTML 应用. 与此同时, 你可以添加不止一个 tsconfig.json 文件, 其中每一个都会作为项目的一部分被构建. 这使得你可以在不使用多个不同项目的情况下为应用的不同部分使用不同的配置.

展示 Visual Studio 中的 tsconfig.json

当项目中添加了 tsconfig.json 文件时, 我们还禁用了项目属性页面. 也就是说所有配置的改变必须在 tsconfig.json 文件中进行.

一些限制

  • 如果你添加了一个 tsconfig.json 文件, 不在其上下文中的 TypeScript 文件不会被编译.
  • Apache Cordova 应用依然有单个 tsconfig.json 文件的限制, 而这个文件必须在根目录或者 scripts 文件夹.
  • 多数项目类型中都没有 tsconfig.json 的模板.

TypeScript 1.7

支持 async/await 编译到 ES6 (Node v4+)

TypeScript 目前在已经原生支持 ES6 generator 的引擎 (比如 Node v4 及以上版本) 上支持异步函数. 异步函数前置 async 关键字; await 会暂停执行, 直到一个异步函数执行后返回的 promise 被 fulfill 后获得它的值.

例子

在下面的例子中, 输入的内容将会延时 400 毫秒逐个打印:

"use strict";

// printDelayed 返回值是一个 'Promise<void>'
async function printDelayed(elements: string[]) {
    for (const element of elements) {
        await delay(400);
        console.log(element);
    }
}

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

printDelayed(["Hello", "beautiful", "asynchronous", "world"]).then(() => {
    console.log();
    console.log("打印每一个内容!");
});

查看 Async Functions 一文了解更多.

支持同时使用 --target ES6--module

TypeScript 1.7 将 ES6 添加到了 --module 选项支持的选项的列表, 当编译到 ES6 时允许指定模块类型. 这让使用具体运行时中你需要的特性更加灵活.

例子

{
    "compilerOptions": {
        "module": "amd",
        "target": "es6"
    }
}

this 类型

在方法中返回当前对象 (也就是 this) 是一种创建链式 API 的常见方式. 比如, 考虑下面的 BasicCalculator 模块:

export default class BasicCalculator {
    public constructor(protected value: number = 0) { }

    public currentValue(): number {
        return this.value;
    }

    public add(operand: number) {
        this.value += operand;
        return this;
    }

    public subtract(operand: number) {
        this.value -= operand;
        return this;
    }

    public multiply(operand: number) {
        this.value *= operand;
        return this;
    }

    public divide(operand: number) {
        this.value /= operand;
        return this;
    }
}

使用者可以这样表述 2 * 5 + 1:

import calc from "./BasicCalculator";

let v = new calc(2)
    .multiply(5)
    .add(1)
    .currentValue();

这使得这么一种优雅的编码方式成为可能; 然而, 对于想要去继承 BasicCalculator 的类来说有一个问题. 想象使用者可能需要编写一个 ScientificCalculator:

import BasicCalculator from "./BasicCalculator";

export default class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }

    public square() {
        this.value = this.value ** 2;
        return this;
    }

    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
}

因为 BasicCalculator 的方法返回了 this, TypeScript 过去推断的类型是 BasicCalculator, 如果在 ScientificCalculator 的实例上调用属于 BasicCalculator 的方法, 类型系统不能很好地处理.

举例来说:

import calc from "./ScientificCalculator";

let v = new calc(0.5)
    .square()
    .divide(2)
    .sin()    // Error: 'BasicCalculator' 没有 'sin' 方法.
    .currentValue();

这已经不再是问题 - TypeScript 现在在类的实例方法中, 会将 this 推断为一个特殊的叫做 this 的类型. this 类型也就写作 this, 可以大致理解为 "方法调用时点左边的类型".

this 类型在描述一些使用了 mixin 风格继承的库 (比如 Ember.js) 的交叉类型:

interface MyType {
    extend<T>(other: T): this & T;
}

ES7 幂运算符

TypeScript 1.7 支持将在 ES7/ES2016 中增加的幂运算符: ****=. 这些运算符会被转换为 ES3/ES5 中的 Math.pow.

举例

var x = 2 ** 3;
var y = 10;
y **= 2;
var z =  -(4 ** 3);

会生成下面的 JavaScript:

var x = Math.pow(2, 3);
var y = 10;
y = Math.pow(y, 2);
var z = -(Math.pow(4, 3));

改进对象字面量解构的检查

TypeScript 1.7 使对象和数组字面量解构初始值的检查更加直观和自然.

当一个对象字面量通过与之对应的对象解构绑定推断类型时:

  • 对象解构绑定中有默认值的属性对于对象字面量来说可选.
  • 对象解构绑定中的属性如果在对象字面量中没有匹配的值, 则该属性必须有默认值, 并且会被添加到对象字面量的类型中.
  • 对象字面量中的属性必须在对象解构绑定中存在.

当一个数组字面量通过与之对应的数组解构绑定推断类型时:

  • 数组解构绑定中的元素如果在数组字面量中没有匹配的值, 则该元素必须有默认值, 并且会被添加到数组字面量的类型中.

举例

// f1 的类型为 (arg?: { x?: number, y?: number }) => void
function f1({ x = 0, y = 0 } = {}) { }

// And can be called as:
f1();
f1({});
f1({ x: 1 });
f1({ y: 1 });
f1({ x: 1, y: 1 });

// f2 的类型为 (arg?: (x: number, y?: number) => void
function f2({ x, y = 0 } = { x: 0 }) { }

f2();
f2({});        // 错误, x 非可选
f2({ x: 1 });
f2({ y: 1 });  // 错误, x 非可选
f2({ x: 1, y: 1 });

装饰器 (decorators) 支持的编译目标版本增加 ES3

装饰器现在可以编译到 ES3. TypeScript 1.7 在 __decorate 函数中移除了 ES5 中增加的 reduceRight. 相关改动也内联了对 Object.getOwnPropertyDescriptorObject.defineProperty 的调用, 并向后兼容, 使 ES5 的输出可以消除前面提到的 Object 方法的重复[1].

TypeScript 1.6

JSX 支持

JSX 是一种可嵌入的类似 XML 的语法. 它将最终被转换为合法的 JavaScript, 但转换的语义和具体实现有关. JSX 随着 React 流行起来, 也出现在其他应用中. TypeScript 1.6 支持 JavaScript 文件中 JSX 的嵌入, 类型检查, 以及直接编译为 JavaScript 的选项.

新的 .tsx 文件扩展名和 as 运算符

TypeScript 1.6 引入了新的 .tsx 文件扩展名. 这一扩展名一方面允许 TypeScript 文件中的 JSX 语法, 一方面将 as 运算符作为默认的类型转换方式 (避免 JSX 表达式和 TypeScript 前置类型转换运算符之间的歧义). 比如:

var x = <any> foo;
// 与如下等价:
var x = foo as any;

使用 React

使用 React 及 JSX 支持, 你需要使用 React 类型声明. 这些类型定义了 JSX 命名空间, 以便 TypeScript 能正确地检查 React 的 JSX 表达式. 比如:

/// <reference path="react.d.ts" />

interface Props {
  name: string;
}

class MyComponent extends React.Component<Props, {}> {
  render() {
    return <span>{this.props.foo}</span>
  }
}

<MyComponent name="bar" />; // 没问题
<MyComponent name={0} />; // 错误, `name` 不是一个字符串

使用其他 JSX 框架

JSX 元素的名称和属性是根据 JSX 命名空间来检验的. 请查看 JSX 页面了解如何为自己的框架定义 JSX 命名空间.

编译输出

TypeScript 支持两种 JSX 模式: preserve (保留) 和 react.

  • preserve 模式将会在输出中保留 JSX 表达式, 使之后的转换步骤可以处理. 并且输出的文件扩展名为 .jsx.
  • react 模式将会生成 React.createElement, 不再需要再通过 JSX 转换即可运行, 输出的文件扩展名为 .js.

查看 JSX 页面了解更多 JSX 在 TypeScript 中的使用.

交叉类型 (intersection types)

TypeScript 1.6 引入了交叉类型作为联合类型 (union types) 逻辑上的补充. 联合类型 A | B 表示一个类型为 AB 的实体, 而交叉类型 A & B 表示一个类型同时为 AB 的实体.

例子

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U> {};
    for (let id in first) {
        result[id] = first[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id];
        }
    }
    return result;
}

var x = extend({ a: "hello" }, { b: 42 });
var s = x.a;
var n = x.b;
type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
interface A { a: string }
interface B { b: string }
interface C { c: string }

var abc: A & B & C;
abc.a = "hello";
abc.b = "hello";
abc.c = "hello";

查看 issue #1256 了解更多.

本地类型声明

本地的类, 接口, 枚举和类型别名现在可以在函数声明中出现. 本地类型为块级作用域, 与 letconst 声明的变量类似. 比如说:

function f() {
    if (true) {
        interface T { x: number }
        let v: T;
        v.x = 5;
    }
    else {
        interface T { x: string }
        let v: T;
        v.x = "hello";
    }
}

推导出的函数返回值类型可能在函数内部声明的. 调用函数的地方无法引用到这样的本地类型, 但是它当然能从类型结构上匹配. 比如:

interface Point {
    x: number;
    y: number;
}

function getPointFactory(x: number, y: number) {
    class P {
        x = x;
        y = y;
    }
    return P;
}

var PointZero = getPointFactory(0, 0);
var PointOne = getPointFactory(1, 1);
var p1 = new PointZero();
var p2 = new PointZero();
var p3 = new PointOne();

本地的类型可以引用类型参数, 本地的类和接口本身即可能是泛型. 比如:

function f3() {
    function f<X, Y>(x: X, y: Y) {
        class C {
            public x = x;
            public y = y;
        }
        return C;
    }
    let C = f(10, "hello");
    let v = new C();
    let x = v.x;  // number
    let y = v.y;  // string
}

类表达式

TypeScript 1.6 增加了对 ES6 类表达式的支持. 在一个类表达式中, 类的名称是可选的, 如果指明, 作用域仅限于类表达式本身. 这和函数表达式可选的名称类似. 在类表达式外无法引用其实例类型, 但是自然也能够从类型结构上匹配. 比如:

let Point = class {
    constructor(public x: number, public y: number) { }
    public length() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }
};
var p = new Point(3, 4);  // p has anonymous class type
console.log(p.length());

继承表达式

TypeScript 1.6 增加了对类继承任意值为一个构造函数的表达式的支持. 这样一来内建的类型也可以在类的声明中被继承.

extends 语句过去需要指定一个类型引用, 现在接受一个可选类型参数的表达式. 表达式的类型必须为有至少一个构造函数签名的构造函数, 并且需要和 extends 语句中类型参数数量一致. 匹配的构造函数签名的返回值类型是类实例类型继承的基类型. 如此一来, 这使得普通的类和与类相似的表达式可以在 extends 语句中使用.

一些例子:

// 继承内建类

class MyArray extends Array<number> { }
class MyError extends Error { }

// 继承表达式类

class ThingA {
    getGreeting() { return "Hello from A"; }
}

class ThingB {
    getGreeting() { return "Hello from B"; }
}

interface Greeter {
    getGreeting(): string;
}

interface GreeterConstructor {
    new (): Greeter;
}

function getGreeterBase(): GreeterConstructor {
    return Math.random() >= 0.5 ? ThingA : ThingB;
}

class Test extends getGreeterBase() {
    sayHello() {
        console.log(this.getGreeting());
    }
}

abstract (抽象的) 类和方法

TypeScript 1.6 为类和它们的方法增加了 abstract 关键字. 一个抽象类允许没有被实现的方法, 并且不能被构造.

例子

abstract class Base {
    abstract getThing(): string;
    getOtherThing() { return 'hello'; }
}

let x = new Base(); // 错误, 'Base' 是抽象的

// 错误, 必须也为抽象类, 或者实现 'getThing' 方法
class Derived1 extends Base { }

class Derived2 extends Base {
    getThing() { return 'hello'; }
    foo() {
        super.getThing();// 错误: 不能调用 'super' 的抽象方法
    }
}

var x = new Derived2(); // 正确
var y: Base = new Derived2(); // 同样正确
y.getThing(); // 正确
y.getOtherThing(); // 正确

泛型别名

TypeScript 1.6 中, 类型别名支持泛型. 比如:

type Lazy<T> = T | (() => T);

var s: Lazy<string>;
s = "eager";
s = () => "lazy";

interface Tuple<A, B> {
    a: A;
    b: B;
}

type Pair<T> = Tuple<T, T>;

更严格的对象字面量赋值检查

为了能发现多余或者错误拼写的属性, TypeScript 1.6 使用了更严格的对象字面量检查. 确切地说, 在将一个新的对象字面量赋值给一个变量, 或者传递给类型非空的参数时, 如果对象字面量的属性在目标类型中不存在, 则会视为错误.

例子

var x: { foo: number };
x = { foo: 1, baz: 2 };  // 错误, 多余的属性 `baz`

var y: { foo: number, bar?: number };
y = { foo: 1, baz: 2 };  // 错误, 多余或者拼错的属性 `baz`

一个类型可以通过包含一个索引签名来显示指明未出现在类型中的属性是被允许的.

var x: { foo: number, [x: string]: any };
x = { foo: 1, baz: 2 };  // 现在 `baz` 匹配了索引签名

ES6 生成器 (generators)

TypeScript 1.6 添加了对于 ES6 输出的生成器支持.

一个生成器函数可以有返回值类型标注, 就像普通的函数. 标注表示生成器函数返回的生成器的类型. 这里有个例子:

function *g(): Iterable<string> {
    for (var i = 0; i < 100; i++) {
        yield ""; // string 可以赋值给 string
    }
    yield * otherStringGenerator(); // otherStringGenerator 必须可遍历, 并且元素类型需要可赋值给 string
}

没有标注类型的生成器函数会有自动推演的类型. 在下面的例子中, 类型会由 yield 语句推演出来:

function *g() {
    for (var i = 0; i < 100; i++) {
        yield ""; // 推导出 string
    }
    yield * otherStringGenerator(); // 推导出 otherStringGenerator 的元素类型
}

async (异步) 函数的试验性支持

TypeScript 1.6 增加了编译到 ES6 时对 async 函数试验性的支持. 异步函数会执行一个异步的操作, 在等待的同时不会阻塞程序的正常运行. 这是通过与 ES6 兼容的 Promise 实现完成的, 并且会将函数体转换为支持在等待的异步操作完成时继续的形式.

async 标记的函数或方法被称作_异步函数_. 这个标记告诉了编译器该函数体需要被转换, 关键字 await 则应该被当做一个一元运算符, 而不是标示符. 一个_异步函数_必须返回类型与 Promise 兼容的值. 返回值类型的推断只能在有一个全局的, 与 ES6 兼容的 Promise 类型时使用.

例子

var p: Promise<number> = /* ... */;
async function fn(): Promise<number> {
  var i = await p; // 暂停执行直到 'p' 得到结果. 'i' 的类型为 "number"
  return 1 + i;
}

var a = async (): Promise<number> => 1 + await p; // 暂停执行.
var a = async () => 1 + await p; // 暂停执行. 使用 --target ES6 选项编译时返回值类型被推断为 "Promise<number>"
var fe = async function(): Promise<number> {
  var i = await p; // 暂停执行知道 'p' 得到结果. 'i' 的类型为 "number"
  return 1 + i;
}

class C {
  async m(): Promise<number> {
    var i = await p; // 暂停执行知道 'p' 得到结果. 'i' 的类型为 "number"
    return 1 + i;
  }

  async get p(): Promise<number> {
    var i = await p; // 暂停执行知道 'p' 得到结果. 'i' 的类型为 "number"
    return 1 + i;
  }
}

每天发布新版本

由于并不算严格意义上的语言变化[2], 每天的新版本可以使用如下命令安装获得:

npm install -g typescript@next

对模块解析逻辑的调整

从 1.6 开始, TypeScript 编译器对于 "commonjs" 的模块解析会使用一套不同的规则. 这些规则 尝试模仿 Node 查找模块的过程. 这就意味着 node 模块可以包含它的类型信息, 并且 TypeScript 编译器可以找到这些信息. 不过用户可以通过使用 --moduleResolution 命令行选项覆盖模块解析规则. 支持的值有:

  • 'classic' - TypeScript 1.6 以前的编译器使用的模块解析规则
  • 'node' - 与 node 相似的模块解析

合并外围类和接口的声明

外围类的实例类型可以通过接口声明来扩展. 类构造函数对象不会被修改. 比如说:

declare class Foo {
    public x : number;
}

interface Foo {
    y : string;
}

function bar(foo : Foo)  {
    foo.x = 1; // 没问题, 在类 Foo 中有声明
    foo.y = "1"; // 没问题, 在接口 Foo 中有声明
}

用户定义的类型收窄函数

TypeScript 1.6 增加了一个新的在 if 语句中收窄变量类型的方式, 作为对 typeofinstanceof 的补充. 用户定义的类型收窄函数的返回值类型标注形式为 x is T, 这里 x 是函数声明中的形参, T 是任何类型. 当一个用户定义的类型收窄函数在 if 语句中被传入某个变量执行时, 该变量的类型会被收窄到 T.

例子

function isCat(a: any): a is Cat {
  return a.name === 'kitty';
}

var x: Cat | Dog;
if(isCat(x)) {
  x.meow(); // 那么, x 在这个代码块内是 Cat 类型
}

tsconfig.jsonexclude 属性的支持

一个没有写明 files 属性的 tsconfig.json 文件 (默认会引用所有子目录下的 *.ts 文件) 现在可以包含一个 exclude 属性, 指定需要在编译中排除的文件或者目录列表. exclude 属性必须是一个字符串数组, 其中每一个元素指定对应的一个文件或者文件夹名称对于 tsconfig.json 文件所在位置的相对路径. 举例来说:

{
    "compilerOptions": {
        "out": "test.js"
    },
    "exclude": [
        "node_modules",
        "test.ts",
        "utils/t2.ts"
    ]
}

exclude 列表不支持通配符. 仅仅可以是文件或者目录的列表.

--init 命令行选项

在一个目录中执行 tsc --init 可以在该目录中创建一个包含了默认值的 tsconfig.json. 可以通过一并传递其他选项来生成初始的 tsconfig.json.

TypeScript 1.5

ES6 模块

TypeScript 1.5 支持 ECMAScript 6 (ES6) 模块. ES6 模块可以看做之前 TypeScript 的外部模块换上了新的语法: ES6 模块是分开加载的源文件, 这些文件还可能引入其他模块, 并且导出部分供外部可访问. ES6 模块新增了几种导入和导出声明. 我们建议使用 TypeScript 开发的库和应用能够更新到新的语法, 但不做强制要求. 新的 ES6 模块语法和 TypeScript 原来的内部和外部模块结构同时被支持, 如果需要也可以混合使用.

导出声明

作为 TypeScript 已有的 export 前缀支持, 模块成员也可以使用单独导出的声明导出, 如果需要, as 语句可以指定不同的导出名称.

interface Stream { ... }
function writeToStream(stream: Stream, data: string) { ... }
export { Stream, writeToStream as write };  // writeToStream 导出为 write

引入声明也可以使用 as 语句来指定一个不同的导入名称. 比如:

import { read, write, standardOutput as stdout } from "./inout";
var s = read(stdout);
write(stdout, s);

作为单独导入的候选项, 命名空间导入可以导入整个模块:

import * as io from "./inout";
var s = io.read(io.standardOutput);
io.write(io.standardOutput, s);

重新导出

使用 from 语句一个模块可以复制指定模块的导出项到当前模块, 而无需创建本地名称.

export { read, write, standardOutput as stdout } from "./inout";

export * 可以用来重新导出另一个模块的所有导出项. 在创建一个聚合了其他几个模块导出项的模块时很方便.

export function transform(s: string): string { ... }
export * from "./mod1";
export * from "./mod2";

默认导出项

一个 export default 声明表示一个表达式是这个模块的默认导出项.

export default class Greeter {
    sayHello() {
        console.log("Greetings!");
    }
}

对应的可以使用默认导入:

import Greeter from "./greeter";
var g = new Greeter();
g.sayHello();

无导入加载

"无导入加载" 可以被用来加载某些只需要其副作用的模块.

import "./polyfills";

了解更多关于模块的信息, 请参见 ES6 模块支持规范.

声明与赋值的解构

TypeScript 1.5 添加了对 ES6 解构声明与赋值的支持.

解构

解构声明会引入一个或多个命名变量, 并且初始化它们的值为对象的属性或者数组的元素对应的值.

比如说, 下面的例子声明了变量 x, yz, 并且分别将它们的值初始化为 getSomeObject().x, getSomeObject().ygetSomeObject().z:

var { x, y, z } = getSomeObject();

解构声明也可以用于从数组中得到值.

var [x, y, z = 10] = getSomeArray();

相似的, 解构可以用在函数的参数声明中:

function drawText({ text = "", location: [x, y] = [0, 0], bold = false }) {
    // 画出文本
}

// 以一个对象字面量为参数调用 drawText
var item = { text: "someText", location: [1,2,3], style: "italics" };
drawText(item);

赋值

解构也可以被用于普通的赋值表达式. 举例来讲, 交换两个变量的值可以被写作一个解构赋值:

var x = 1;
var y = 2;
[x, y] = [y, x];

namespace (命名空间) 关键字

过去 TypeScript 中 module 关键字既可以定义 "内部模块", 也可以定义 "外部模块"; 这让刚刚接触 TypeScript 的开发者有些困惑. "内部模块" 的概念更接近于大部分人眼中的命名空间; 而 "外部模块" 对于 JS 来讲, 现在也就是模块了.

注意: 之前定义内部模块的语法依然被支持.

之前:

module Math {
    export function add(x, y) { ... }
}

之后:

namespace Math {
    export function add(x, y) { ... }
}

letconst 的支持

ES6 的 letconst 声明现在支持编译到 ES3 和 ES5.

Const

const MAX = 100;

++MAX; // 错误: 自增/减运算符不能用于一个常量

块级作用域

if (true) {
  let a = 4;
  // 使用变量 a
}
else {
  let a = "string";
  // 使用变量 a
}

alert(a); // 错误: 变量 a 在当前作用域未定义

for...of 的支持

TypeScript 1.5 增加了 ES6 for...of 循环编译到 ES3/ES5 时对数组的支持, 以及编译到 ES6 时对满足 Iterator 接口的全面支持.

例子

TypeScript 编译器会转译 for...of 数组到具有语义的 ES3/ES5 JavaScript (如果被设置为编译到这些版本).

for (var v of expr) { }

会输出为:

for (var _i = 0, _a = expr; _i < _a.length; _i++) {
    var v = _a[_i];
}

装饰器

TypeScript 装饰器是局域 ES7 装饰器 提案的.

一个装饰器是:

  • 一个表达式
  • 并且值为一个函数
  • 接受 target, name, 以及属性描述对象作为参数
  • 可选返回一个会被应用到目标对象的属性描述对象

了解更多, 请参见 装饰器 提案.

例子

装饰器 readonlyenumerable(false) 会在属性 method 添加到类 C 上之前被应用. 这使得装饰器可以修改其实现, 具体到这个例子, 设置了 descriptorwritable: false 以及 enumerable: false.

class C {
  @readonly
  @enumerable(false)
  method() { }
}

function readonly(target, key, descriptor) {
    descriptor.writable = false;
}

function enumerable(value) {
  return function (target, key, descriptor) {
     descriptor.enumerable = value;
  }
}

计算属性

使用动态的属性初始化一个对象可能会很麻烦. 参考下面的例子:

type NeighborMap = { [name: string]: Node };
type Node = { name: string; neighbors: NeighborMap;}

function makeNode(name: string, initialNeighbor: Node): Node {
    var neighbors: NeighborMap = {};
    neighbors[initialNeighbor.name] = initialNeighbor;
    return { name: name, neighbors: neighbors };
}

这里我们需要创建一个包含了 neighbor-map 的变量, 便于我们初始化它. 使用 TypeScript 1.5, 我们可以让编译器来干重活:

function makeNode(name: string, initialNeighbor: Node): Node {
    return {
        name: name,
        neighbors: {
            [initialNeighbor.name]: initialNeighbor
        }
    }
}

指出 UMDSystem 模块输出

作为 AMDCommonJS 模块加载器的补充, TypeScript 现在支持输出为 UMD (Universal Module Definition) 和 System 模块的格式.

用法:

tsc --module umd

以及

tsc --module system

Unicode 字符串码位转义

ES6 中允许用户使用单个转义表示一个 Unicode 码位.

举个例子, 考虑我们需要转义一个包含了字符 '𠮷' 的字符串. 在 UTF-16/USC2 中, '𠮷' 被表示为一个代理对, 意思就是它被编码为一对 16 位值的代码单元, 具体来说是 0xD8420xDFB7. 之前这意味着你必须将该码位转义为 "\uD842\uDFB7". 这样做有一个重要的问题, 就事很难讲两个独立的字符同一个代理对区分开来.

通过 ES6 的码位转义, 你可以在字符串或模板字符串中清晰地通过一个转义表示一个确切的字符: "\u{20bb7}". TypeScript 在编译到 ES3/ES5 时会将该字符串输出为 "\uD842\uDFB7".

标签模板字符串编译到 ES3/ES5

TypeScript 1.4 中, 我们添加了模板字符串编译到所有 ES 版本的支持, 并且支持标签模板字符串编译到 ES6. 得益于 @ivogabe 的大量付出, 我们填补了标签模板字符串对编译到 ES3/ES5 的支持.

当编译到 ES3/ES5 时, 下面的代码:

function oddRawStrings(strs: TemplateStringsArray, n1, n2) {
    return strs.raw.filter((raw, index) => index % 2 === 1);
}

oddRawStrings `Hello \n${123} \t ${456}\n world`

会被输出为:

function oddRawStrings(strs, n1, n2) {
    return strs.raw.filter(function (raw, index) {
        return index % 2 === 1;
    });
}
(_a = ["Hello \n", " \t ", "\n world"], _a.raw = ["Hello \\n", " \\t ", "\\n world"], oddRawStrings(_a, 123, 456));
var _a;

AMD 可选依赖名称

/// <amd-dependency path="x" /> 会告诉编译器需要被注入到模块 require 方法中的非 TS 模块依赖; 然而在 TS 代码中无法使用这个模块.

新的 amd-dependency name 属性允许为 AMD 依赖传递一个可选的名称.

/// <amd-dependency path="legacy/moduleA" name="moduleA"/>
declare var moduleA:MyType
moduleA.callStuff()

生成的 JS 代码:

define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) {
    moduleA.callStuff()
});

通过 tsconfig.json 指示一个项目

通过添加 tsconfig.json 到一个目录指明这是一个 TypeScript 项目的根目录. tsconfig.json 文件指定了根文件以及编译项目需要的编译器选项. 一个项目可以由以下方式编译:

  • 调用 tsc 并不指定输入文件, 此时编译器会从当前目录开始往上级目录寻找 tsconfig.json 文件.
  • 调用 tsc 并不指定输入文件, 使用 -project (或者 -p) 命令行选项指定包含了 tsconfig.json 文件的目录.

例子

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "sourceMap": true,
    }
}

参见 tsconfig.json wiki 页面 查看更多信息.

--rootDir 命令行选项

选项 --outDir 在输出中会保留输入的层级关系. 编译器将所有输入文件共有的最长路径作为根路径; 并且在输出中应用对应的子层级关系.

有的时候这并不是期望的结果, 比如输入 FolderA\FolderB\1.tsFolderA\FolderB\2.ts, 输出结构会是 FolderA\FolderB\ 对应的结构. 如果输入中新增 FolderA\3.ts 文件, 输出的结构将突然变为 FolderA\ 对应的结构.

--rootDir 指定了会输出对应结构的输入目录, 不再通过计算获得.

--noEmitHelpers 命令行选项

TypeScript 编译器在需要的时候会输出一些像 __extends 这样的工具函数. 这些函数会在使用它们的所有文件中输出. 如果你想要聚合所有的工具函数到同一个位置, 或者覆盖默认的行为, 使用 --noEmitHelpers 来告知编译器不要输出它们.

--newLine 命令行选项

默认输出的换行符在 Windows 上是 \r\n, 在 *nix 上是 \n. --newLine 命令行标记可以覆盖这个行为, 并指定输出文件中使用的换行符.

--inlineSourceMap and inlineSources 命令行选项

--inlineSourceMap 将内嵌源文件映射到 .js 文件, 而不是在单独的 .js.map 文件中. --inlineSources 允许进一步将 .ts 文件内容包含到输出文件中.

TypeScript 1.4

联合类型

概述

联合类型有助于表示一个值的类型可以是多种类型之一的情况。比如,有一个API接命令行传入string类型,string[]类型或者是一个返回string的函数。你就可以这样写:

interface RunOptions {
   program: string;
   commandline: string[]|string|(() => string);
}

给联合类型赋值也很直观 -- 只要这个值能满足联合类型中任意一个类型那么就可以赋值给这个联合类型:

var opts: RunOptions = /* ... */;
opts.commandline = '-hello world'; // OK
opts.commandline = ['-hello', 'world']; // OK
opts.commandline = [42]; // Error, 数字不是字符串或字符串数组

当读取联合类型时,你可以访问类型共有的属性:

if(opts.length === 0) { // OK, string和string[]都有'length'属性
  console.log("it's empty");
}

使用类型保护,你可以轻松地使用联合类型:

function formatCommandline(c: string|string[]) {
    if(typeof c === 'string') {
        return c.trim();
    } else {
        return c.join(' ');
    }
}

严格的泛型

随着联合类型可以表示有很多类型的场景,我们决定去改进泛型调用的规范性。之前,这段代码编译不会报错(出乎意料):

function equal<T>(lhs: T, rhs: T): boolean {
  return lhs === rhs;
}

// 之前没有错误
// 现在会报错:在string和number之前没有最佳的基本类型
var e = equal(42, 'hello');

通过联合类型,你可以指定你想要的行为,在函数定义时或在调用的时候:

// 'choose' function where types must match
function choose1<T>(a: T, b: T): T { return Math.random() > 0.5 ? a : b }
var a = choose1('hello', 42); // Error
var b = choose1<string|number>('hello', 42); // OK

// 'choose' function where types need not match
function choose2<T, U>(a: T, b: U): T|U { return Math.random() > 0.5 ? a : b }
var c = choose2('bar', 'foo'); // OK, c: string
var d = choose2('hello', 42); // OK, d: string|number

更好的类型推断

当一个集合里有多种类型的值时,联合类型会为数组或其它地方提供更好的类型推断:

var x = [1, 'hello']; // x: Array<string|number>
x[0] = 'world'; // OK
x[0] = false; // Error, boolean is not string or number

let 声明

在JavaScript里,var声明会被“提升”到所在作用域的顶端。这可能会引发一些让人不解的bugs:

console.log(x); // meant to write 'y' here
/* later in the same block */
var x = 'hello';

TypeScript已经支持新的ES6的关键字let,声明一个块级作用域的变量。一个let变量只能在声明之后的位置被引用,并且作用域为声明它的块里:

if(foo) {
    console.log(x); // Error, cannot refer to x before its declaration
    let x = 'hello';
} else {
    console.log(x); // Error, x is not declared in this block
}

let只在设置目标为ECMAScript 6 (--target ES6)时生效。

const 声明

另一个TypeScript支持的ES6里新出现的声明类型是const。不能给一个const类型变量赋值,只能在声明的时候初始化。这对于那些在初始化之后就不想去改变它的值的情况下是很有帮助的:

const halfPi = Math.PI / 2;
halfPi = 2; // Error, can't assign to a `const`

const只在设置目标为ECMAScript 6 (--target ES6)时生效。

模版字符串

TypeScript现已支持ES6模块字符串。通过它可以方便地在字符串中嵌入任何表达式:

var name = "TypeScript";
var greeting  = `Hello, ${name}! Your name has ${name.length} characters`;

当编译目标为ES6之前的版本时,这个字符串被分解为:

var name = "TypeScript!";
var greeting = "Hello, " + name + "! Your name has " + name.length + " characters";

类型守护

JavaScript常用模式之一是在运行时使用typeofinstanceof检查表达式的类型。 在if语句里使用它们的时候,TypeScript可以识别出这些条件并且随之改变类型推断的结果。

使用typeof来检查一个变量:

var x: any = /* ... */;
if(typeof x === 'string') {
    console.log(x.subtr(1)); // Error, 'subtr' does not exist on 'string'
}
// x is still any here
x.unknown(); // OK

结合联合类型使用typeofelse

var x: string|HTMLElement = /* ... */;
if(typeof x === 'string') {
    // x is string here, as shown above
} else {
    // x is HTMLElement here
    console.log(x.innerHTML);
}

结合类和联合类型使用instanceof

class Dog { woof() { } }
class Cat { meow() { } }
var pet: Dog|Cat = /* ... */;
if(pet instanceof Dog) {
    pet.woof(); // OK
} else {
    pet.woof(); // Error
}

类型别名

你现在可以使用type关键字来为类型定义一个“别名”:

type PrimitiveArray = Array<string|number|boolean>;
type MyNumber = number;
type NgScope = ng.IScope;
type Callback = () => void;

类型别名与其原始的类型完全一致;它们只是简单的替代名。

const enum(完全嵌入的枚举)

枚举很有帮助,但是有些程序实际上并不需要它生成的代码并且想要将枚举变量所代码的数字值直接替换到对应位置上。新的const enum声明与正常的enum在类型安全方面具有同样的作用,只是在编译时会清除掉。

const enum Suit { Clubs, Diamonds, Hearts, Spades }
var d = Suit.Diamonds;

Compiles to exactly:

var d = 1;

TypeScript也会在可能的情况下计算枚举值:

enum MyFlags {
  None = 0,
  Neat = 1,
  Cool = 2,
  Awesome = 4,
  Best = Neat | Cool | Awesome
}
var b = MyFlags.Best; // emits var b = 7;

-noEmitOnError 命令行选项

TypeScript编译器的默认行为是当存在类型错误(比如,将string类型赋值给number类型)时仍会生成.js文件。这在构建服务器上或是其它场景里可能会是不想看到的情况,因为希望得到的是一次“纯净”的构建。新的noEmitOnError标记可以阻止在编译时遇到错误的情况下继续生成.js代码。

它现在是MSBuild工程的默认行为;这允许MSBuild持续构建以我们想要的行为进行,输出永远是来自纯净的构建。

AMD 模块名

默认情况下AMD模块以匿名形式生成。这在使用其它工具(比如,r.js)处理生成的模块的时可能会带来麻烦。

新的amd-module name标签允许给编译器传入一个可选的模块名:

//// [amdModule.ts]
///<amd-module name='NamedModule'/>
export class C {
}

结果会把NamedModule赋值成模块名,做为调用AMDdefine的一部分:

//// [amdModule.js]
define("NamedModule", ["require", "exports"], function (require, exports) {
    var C = (function () {
        function C() {
        }
        return C;
    })();
    exports.C = C;
});

TypeScript 1.3

受保护的

类里面新的protected修饰符作用与其它语言如C++,C#和Java中的一样。一个类的protected成员只在这个类的子类中可见:

class Thing {
  protected doSomething() { /* ... */ }
}

class MyThing extends Thing {
  public myMethod() {
    // OK,可以在子类里访问受保护的成员
    this.doSomething();
  }
}
var t = new MyThing();
t.doSomething(); // Error,不能在类外部访问受保护成员

元组类型

元组类型表示一个数组,其中元素的类型都是已知的,但是不一样是同样的类型。比如,你可能想要表示一个第一个元素是string类型第二个元素是number类型的数组:

// Declare a tuple type
var x: [string, number];
// 初始化
x = ['hello', 10]; // OK
// 错误的初始化
x = [10, 'hello']; // Error

但是访问一个已知的索引,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number'没有'substr'方法

注意在TypeScript1.4里,当访问超出已知索引的元素时,会返回联合类型:

x[3] = 'world'; // OK
console.log(x[5].toString()); // OK, 'string'和'number'都有toString
x[6] = true; // Error, boolean不是number或string

TypeScript 1.1

改进性能

1.1版本的编译器速度比所有之前发布的版本快4倍。阅读这篇博客里的有关图表

更好的模块可见性规则

TypeScript现在只在使用--declaration标记时才严格强制模块里类型的可见性。这在Angular里很有用,例如:

module MyControllers {
  interface ZooScope extends ng.IScope {
    animals: Animal[];
  }
  export class ZooController {
    // Used to be an error (cannot expose ZooScope), but now is only
    // an error when trying to generate .d.ts files
    constructor(public $scope: ZooScope) { }
    /* more code */
  }
}

Breaking Changes

TypeScript 3.6

类成员的 constructor 现在被叫做 Constructors

根据 ECMAScript 规范,使用名为 constructor 的方法的类声明现在是构造函数,无论它们是使用标识符名称还是字符串名称声明。

class C {
  "constructor"() {
    console.log("现在我是构造函数了。");
  }
}

一个值得注意的例外,以及此改变的解决方法是使用名称计算结果为 constructor 的计算属性。

class D {
  ["constructor"]() {
    console.log("我只是一个纯粹的方法,不是构造函数!")
  }
}

DOM 定义更新

lib.dom.d.ts 中移除或者修改了大量的定义。其中包括(但不仅限于)以下这些:

  • 全局的 window 不再定义为 Window,它被更明确的定义 type Window & typeof globalThis 替代。在某些情况下,将它作为 typeof window 更好。
  • GlobalFetch 已经被移除。使用 WindowOrWorkerGlobalScrope 替代。
  • Navigator 上明确的非标准的属性已经被移除了。
  • experimental-webgl 上下文已经被移除了。使用 webglwebgl2 替代。

如果你认为其中的改变已经制造了错误,请提交一个 issue

JSDoc 注释不再合并

在 JavaScript 文件中,TypeScript 只会在 JSDoc 注释之前立即查询以确定声明的类型。

/**
 * @param {string} arg
 */
/**
 * 你的其他注释信息
 */
function whoWritesFunctionsLikeThis(arg) {
  // 'arg' 是 'any' 类型
}

关键字不能包含转义字符

之前的版本允许关键字包含转义字符。TypeScript 3.6 不允许。

while (true) {
  \u0063ontinue;
//  ~~~~~~~~~~~~~
// 错误!关键字不能包含转义字符
}

参考

TypeScript 3.5

lib.d.ts 包含了 Omit 辅助类型

TypeScript 3.5 包含一个 Omit 辅助类型。

因此, 你项目中任何全局定义的 Omit 将产生以下错误信息:

Duplicate identifier 'Omit'.

两个变通的方法可以在这里使用:

  1. 删除重复定义的并使用 lib.d.ts 提供的。
  2. 从模块中导出定义避免全局冲突。现有的用法可以使用 import 直接引用项目的旧 Omit 类型。

TypeScript 3.4

顶级 this 现在有类型了

顶级 this 的类型现在被分配为 typeof globalThis 而不是 any

因此, 在 noImplicitAny 下访问 this 上的未知值,你可能收到错误提示。

// 在 `noImplicitAny` 下,以前可以,现在不行
this.whargarbl = 10;

请注意,在 noImplicitThis 下编译的代码不会在此处遇到任何更改。

泛型参数的传递

在某些情况下,TypeScript 3.4 的推断改进可能会产生泛型的函数,而不是那些接收并返回其约束的函数(通常是 {})。

declare function compose<T, U, V>(f: (arg: T) => U, g: (arg: U) => V): (arg: T) => V;

function list<T>(x: T) { return [x]; }
function box<T>(value: T) { return { value }; }

let f = compose(list, box);
let x = f(100)

// 在 TypeScript 3.4 中, 'x.value' 的类型为
//
//   number[]
//
// 但是在之前的版本中类型为
//
//   {}[]
//
// 因此,插入一个 `string` 类型是错误的
x.value.push("hello");

x 上的显式类型注释可以清除这个错误。

上下文返回类型作为上下文参数类型传入

TypeScript 现在使用函数调用时传入的类型(如下例中的 then)作为函数上下文参数类型(如下例中的箭头函数)。

function isEven(prom: Promise<number>): Promise<{ success: boolean }> {
  return prom.then<{success: boolean}>((x) => {
    return x % 2 === 0 ?
      { success: true } :
      Promise.resolve({ success: false });
    });
}

这通常是一种改进,但在上面的例子中,它导致 truefalse 获取不合需要的字面量类型。

Argument of type '(x: number) => Promise<{ success: false; }> | { success: true; }' is not assignable to parameter of type '(value: number) => { success: false; } | PromiseLike<{ success: false; }>'.
  Type 'Promise<{ success: false; }> | { success: true; }' is not assignable to type '{ success: false; } | PromiseLike<{ success: false; }>'.
    Type '{ success: true; }' is not assignable to type '{ success: false; } | PromiseLike<{ success: false; }>'.
      Type '{ success: true; }' is not assignable to type '{ success: false; }'.
        Types of property 'success' are incompatible.

合适的解决方法是将类型参数添加到适当的调用——本例中的 then 方法调用。

function isEven(prom: Promise<number>): Promise<{ success: boolean }> {
  //               vvvvvvvvvvvvvvvvvv
  return prom.then<{success: boolean}>((x) => {
    return x % 2 === 0 ?
      { success: true } :
      Promise.resolve({ success: false });
  });
}

strictFunctionTypes 之外一致性推断优先

在 TypeScript 3.3 中,关闭 --strictFunctionTypes 选项时,假定使用 interface 声明的泛型类型在其类型参数方面始终是协变的。对于函数类型,通常无法观察到此行为。

但是,对于带有 keyof 状态的类型参数的泛型 interface 类型——逆变用法——这些类型表现不正确。

在 TypeScript 3.4 中,现在可以在所有情况下正确探测使用 interface 声明的类型的变动。

这导致一个可见的重大变更,只要有类型参数的接口使用了 keyof(包括诸如 Record<K, T> 之类的地方,这是涉及 keyof K 的类型别名)。下例就是这样一个可能的变更。

interface HasX { x: any }
interface HasY { y: any }

declare const source: HasX | HasY;
declare const properties: KeyContainer<HasX>;

interface KeyContainer<T> {
  key: keyof T;
}

function readKey<T>(source: T, prop: KeyContainer<T>) {
  console.log(source[prop.key])
}

// 这个调用应该被拒绝,因为我们可能会这样做
// 错误地从 'HasY' 中读取 'x'。它现在恰当的提示错误。
readKey(source, properties);

此错误很可能表明原代码存在问题。

参考

TypeScript 3.2

lib.d.ts 更新

wheelDelta 和它的小伙伴们被移除了。

wheelDeltaXwheelDeltawheelDeltaZ 全都被移除了,因为他们在 WheelEvents 上是废弃的属性。

解决办法:使用 deltaXdeltaYdeltaZ 代替。

更具体的类型

根据 DOM 规范的描述,某些参数现在接受更具体的类型,不再接受 null

参考

TypeScript 3.1

一些浏览器厂商特定的类型从lib.d.ts中被移除

TypeScript内置的.d.ts库(lib.d.ts等)现在会部分地从DOM规范的Web IDL文件中生成。 因此有一些浏览器厂商特定的类型被移除了。

点击这里查看被移除类型的完整列表:

* `CanvasRenderingContext2D.mozImageSmoothingEnabled` * `CanvasRenderingContext2D.msFillRule` * `CanvasRenderingContext2D.oImageSmoothingEnabled` * `CanvasRenderingContext2D.webkitImageSmoothingEnabled` * `Document.caretRangeFromPoint` * `Document.createExpression` * `Document.createNSResolver` * `Document.execCommandShowHelp` * `Document.exitFullscreen` * `Document.exitPointerLock` * `Document.focus` * `Document.fullscreenElement` * `Document.fullscreenEnabled` * `Document.getSelection` * `Document.msCapsLockWarningOff` * `Document.msCSSOMElementFloatMetrics` * `Document.msElementsFromRect` * `Document.msElementsFromPoint` * `Document.onvisibilitychange` * `Document.onwebkitfullscreenchange` * `Document.onwebkitfullscreenerror` * `Document.pointerLockElement` * `Document.queryCommandIndeterm` * `Document.URLUnencoded` * `Document.webkitCurrentFullScreenElement` * `Document.webkitFullscreenElement` * `Document.webkitFullscreenEnabled` * `Document.webkitIsFullScreen` * `Document.xmlEncoding` * `Document.xmlStandalone` * `Document.xmlVersion` * `DocumentType.entities` * `DocumentType.internalSubset` * `DocumentType.notations` * `DOML2DeprecatedSizeProperty` * `Element.msContentZoomFactor` * `Element.msGetUntransformedBounds` * `Element.msMatchesSelector` * `Element.msRegionOverflow` * `Element.msReleasePointerCapture` * `Element.msSetPointerCapture` * `Element.msZoomTo` * `Element.onwebkitfullscreenchange` * `Element.onwebkitfullscreenerror` * `Element.webkitRequestFullScreen` * `Element.webkitRequestFullscreen` * `ElementCSSInlineStyle` * `ExtendableEventInit` * `ExtendableMessageEventInit` * `FetchEventInit` * `GenerateAssertionCallback` * `HTMLAnchorElement.Methods` * `HTMLAnchorElement.mimeType` * `HTMLAnchorElement.nameProp` * `HTMLAnchorElement.protocolLong` * `HTMLAnchorElement.urn` * `HTMLAreasCollection` * `HTMLHeadElement.profile` * `HTMLImageElement.msGetAsCastingSource` * `HTMLImageElement.msGetAsCastingSource` * `HTMLImageElement.msKeySystem` * `HTMLImageElement.msPlayToDisabled` * `HTMLImageElement.msPlayToDisabled` * `HTMLImageElement.msPlayToPreferredSourceUri` * `HTMLImageElement.msPlayToPreferredSourceUri` * `HTMLImageElement.msPlayToPrimary` * `HTMLImageElement.msPlayToPrimary` * `HTMLImageElement.msPlayToSource` * `HTMLImageElement.msPlayToSource` * `HTMLImageElement.x` * `HTMLImageElement.y` * `HTMLInputElement.webkitdirectory` * `HTMLLinkElement.import` * `HTMLMetaElement.charset` * `HTMLMetaElement.url` * `HTMLSourceElement.msKeySystem` * `HTMLStyleElement.disabled` * `HTMLSummaryElement` * `MediaQueryListListener` * `MSAccountInfo` * `MSAudioLocalClientEvent` * `MSAudioLocalClientEvent` * `MSAudioRecvPayload` * `MSAudioRecvSignal` * `MSAudioSendPayload` * `MSAudioSendSignal` * `MSConnectivity` * `MSCredentialFilter` * `MSCredentialParameters` * `MSCredentials` * `MSCredentialSpec` * `MSDCCEvent` * `MSDCCEventInit` * `MSDelay` * `MSDescription` * `MSDSHEvent` * `MSDSHEventInit` * `MSFIDOCredentialParameters` * `MSIceAddrType` * `MSIceType` * `MSIceWarningFlags` * `MSInboundPayload` * `MSIPAddressInfo` * `MSJitter` * `MSLocalClientEvent` * `MSLocalClientEventBase` * `MSNetwork` * `MSNetworkConnectivityInfo` * `MSNetworkInterfaceType` * `MSOutboundNetwork` * `MSOutboundPayload` * `MSPacketLoss` * `MSPayloadBase` * `MSPortRange` * `MSRelayAddress` * `MSSignatureParameters` * `MSStatsType` * `MSStreamReader` * `MSTransportDiagnosticsStats` * `MSUtilization` * `MSVideoPayload` * `MSVideoRecvPayload` * `MSVideoResolutionDistribution` * `MSVideoSendPayload` * `NotificationEventInit` * `PushEventInit` * `PushSubscriptionChangeInit` * `RTCIdentityAssertionResult` * `RTCIdentityProvider` * `RTCIdentityProviderDetails` * `RTCIdentityValidationResult` * `Screen.deviceXDPI` * `Screen.logicalXDPI` * `SVGElement.xmlbase` * `SVGGraphicsElement.farthestViewportElement` * `SVGGraphicsElement.getTransformToElement` * `SVGGraphicsElement.nearestViewportElement` * `SVGStylable` * `SVGTests.hasExtension` * `SVGTests.requiredFeatures` * `SyncEventInit` * `ValidateAssertionCallback` * `WebKitDirectoryEntry` * `WebKitDirectoryReader` * `WebKitEntriesCallback` * `WebKitEntry` * `WebKitErrorCallback` * `WebKitFileCallback` * `WebKitFileEntry` * `WebKitFileSystem` * `Window.clearImmediate` * `Window.msSetImmediate` * `Window.setImmediate`

推荐:

如果你的运行时能够保证这些名称是可用的(比如一个仅针对IE的应用),那么可以在本地添加那些声明,例如:

对于Element.msMatchesSelector,在本地的dom.ie.d.ts文件里添加如下代码:

interface Element {
    msMatchesSelector(selectors: string): boolean;
}

相似地,若要添加clearImmediatesetImmediate,你可以在本地的dom.ie.d.ts里添加Window声明:

interface Window {
    clearImmediate(handle: number): void;
    setImmediate(handler: (...args: any[]) => void): number;
    setImmediate(handler: any, ...args: any[]): number;
}

细化的函数现在会使用{}Object和未约束的泛型参数的交叉类型

下面的代码如今会提示x不能被调用:

function foo<T>(x: T | (() => string)) {
    if (typeof x === "function") {
        x();
//      ~~~
// Cannot invoke an expression whose type lacks a call signature. Type '(() => string) | (T & Function)' has no compatible call signatures.
    }
}

这是因为,不同于以前的T会被细化掉,如今T会被扩展成T & Function。 然而,因为这个类型没有声明调用签名,类型系统无法找到通用的调用签名可以适用于T & Function() => string

因此,考虑使用一个更确切的类型,而不是{}Object,并且考虑给T添加额外的约束条件。

TypeScript 3.0

保留关键字 unknown

unknown 现在是一个保留类型名称,因为它现在是一个内置类型。为了支持新引入的 unknown 类型,取决于你对 unknown 的使用方式,你可能需要完全移除变量申明,或者将其重命名。

未开启 strictNullChecks 时,与 null/undefined 交叉的类型会简化到 null/undefined

关闭 strictNullChecks 时,下例中 A 的类型为 null,而 B 的类型为 undefined

type A = { a: number } & null;      // null
type B = { a: number } & undefined; // undefined

这是因为 TypeScript 3.0 更适合分别简化交叉类型和联合类型中的子类型和超类型。但是,因为当 strictNullChecks 关闭时,nullundefined 都被认为是所有其他类型的子类型,与某种对象类型的交集将始终简化为 nullundefined

建议

如果你在类型交叉的情况下依赖 nullundefined 作为单位元,你应该寻找一种方法来使用 unknown 而不是无论它们在哪里都是 nullundefined

参考

TypeScript 2.9

keyof 现在包括 stringnumbersymbol 键名

TypeScript 2.9 将索引类型泛化为包括 numbersymbol 命名属性。以前,keyof 运算符和映射类型仅支持 string 命名属性。

function useKey<T, K extends keyof T>(o: T, k: K) {
  var name: string = k;  // 错误: keyof T 不能分配给 `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 编译器选项禁用新行为。

剩余参数后面不允许尾后逗号

以下代码是一个自 #22262 开始的编译器错误:

function f(
  a: number,
  ...b: number[], // 违规的尾随逗号
) {}

剩余参数上的尾随逗号不是有效的 JavaScript,并且,这个语法现在在 TypeScript 中也是一个错误。

strictNullChecks 中,无类型约束参数不再分配给 object

以下代码是自24013起在 strickNullChecks 下出现的编译器错误:

function f<T>(x: T) {
  const y: object | null | undefined = x;
}

它可以用任意类型(例如,stringnumber )来实现,因此允许它是不正确的。 如果您遇到此问题,请将您的类型参数约束为 object 以仅允许对象类型。如果想允许任何类型,使用 {} 进行比较而不是 object

参考

TypeScript 2.8

--noUnusedParameters下检查未使用的类型参数

根据 #20568,未使用的类型参数之前在--noUnusedLocals下报告,但现在报告在--noUnusedParameters下。

lib.d.ts中删除了一些Microsoft 专用的类型

从DOM定义中删除一些Microsoft 专用的类型以更好地与标准对齐。 删除的类型包括:

  • MSApp
  • MSAppAsyncOperation
  • MSAppAsyncOperationEventMap
  • MSBaseReader
  • MSBaseReaderEventMap
  • MSExecAtPriorityFunctionCallback
  • MSHTMLWebViewElement
  • MSManipulationEvent
  • MSRangeCollection
  • MSSiteModeEvent
  • MSUnsafeFunctionCallback
  • MSWebViewAsyncOperation
  • MSWebViewAsyncOperationEventMap
  • MSWebViewSettings

HTMLObjectElement不再具有alt属性

根据 #21386,DOM库已更新以反映WHATWG标准。

如果需要继续使用alt属性,请考虑通过全局范围中的接口合并重新打开HTMLObjectElement

// Must be in a global .ts file or a 'declare global' block.
interface HTMLObjectElement {
    alt: string;
}

TypeScript 2.7

完整的破坏性改动列表请到这里查看:breaking change issues.

元组现在具有固定长度的属性

以下代码用于没有编译错误:

var pair: [number, number] = [1, 2];
var triple: [number, number, number] = [1, 2, 3];
pair = triple;

但是,这_是_一个错误:

triple = pair;

现在,相互赋值是一个错误。 这是因为元组现在有一个长度属性,其类型是它们的长度。 所以pair.length: 2,但是triple.length: 3

请注意,之前允许某些非元组模式,但现在不再允许:

const struct: [string, number] = ['key'];
for (const n of numbers) {
  struct.push(n);
}

对此最好的解决方法是创建扩展Array的自己的类型:

interface Struct extends Array<string | number> {
  '0': string;
  '1'?: number;
}
const struct: Struct = ['key'];
for (const n of numbers) {
  struct.push(n);
}

allowSyntheticDefaultImports下,对于TS和JS文件来说默认导入的类型合成不常见

在过去,我们在类型系统中合成一个默认导入,用于TS或JS文件,如下所示:

export const foo = 12;

意味着模块的类型为{foo: number, default: {foo: number}}。 这是错误的,因为文件将使用__esModule标记发出,因此在加载文件时没有流行的模块加载器会为它创建合成默认值,并且类型系统推断的default成员永远不会在运行时存在。现在我们在ESModuleInterop标志下的发出中模拟了这个合成默认行为,我们收紧了类型检查器的行为,以匹配你期望在运行时所看到的内容。如果运行时没有其他工具的介入,此更改应仅指出错误的错误默认导入用法,应将其更改为命名空间导入。

更严格地检查索引访问泛型类型约束

以前,仅当类型具有索引签名时才计算索引访问类型的约束,否则它是any。这样就可以取消选中无效赋值。在TS 2.7.1中,编译器在这里有点聪明,并且会将约束计算为此处所有可能属性的并集。

interface O {
    foo?: string;
}

function fails<K extends keyof O>(o: O, k: K) {
    var s: string = o[k]; // Previously allowed, now an error
                          // string | undefined is not assignable to a string
}

in表达式被视为类型保护

对于n in x表达式,其中n是字符串文字或字符串文字类型而x是联合类型,"true"分支缩小为具有可选或必需属性n的类型,并且 "false"分支缩小为具有可选或缺少属性n的类型。 如果声明类型始终具有属性n,则可能导致在false分支中将变量的类型缩小为never的情况。

var x: { foo: number };

if ("foo" in x) {
    x; // { foo: number }
}
else {
    x; // never
}

在条件运算符中不减少结构上相同的类

以前在结构上相同的类在条件或||运算符中被简化为最佳公共类型。现在这些类以联合类型维护,以便更准确地检查instanceof运算符。

class Animal {

}

class Dog {
    park() { }
}

var a = Math.random() ? new Animal() : new Dog();
// typeof a now Animal | Dog, previously Animal

CustomEvent现在是一个泛型类型

CustomEvent现在有一个details属性类型的类型参数。如果要从中扩展,则需要指定其他类型参数。

class MyCustomEvent extends CustomEvent {
}

应该成为

class MyCustomEvent extends CustomEvent<any> {
}

TypeScript 2.6

完整的破坏性改动列表请到这里查看:breaking change issues.

只写引用未使用

以下代码用于没有编译错误:

function f(n: number) {
    n = 0;
}

class C {
    private m: number;
    constructor() {
        this.m = 0;
    }
}

现在,当启用--noUnusedLocals--noUnusedParameters编译器选项时,nm都将被标记为未使用,因为它们的值永远不会被_读_ 。以前TypeScript只会检查它们的值是否被_引用_。

此外,仅在其自己的实体中调用的递归函数被视为未使用。

function f() {
    f(); // Error: 'f' is declared but its value is never read
}

环境上下文中的导出赋值中禁止使用任意表达式

以前,像这样的结构

declare module "foo" {
    export default "some" + "string";
}

在环境上下文中未被标记为错误。声明文件和环境模块中通常禁止使用表达式,因为typeof之类的意图不明确,因此这与我们在这些上下文中的其他地方处理可执行代码不一致。现在,任何不是标识符或限定名称的内容都会被标记为错误。为具有上述值形状的模块制作DTS的正确方法如下:

declare module "foo" {
    const _default: string;
    export default _default;
}

编译器已经生成了这样的定义,因此这只应该是手工编写的定义的问题。

TypeScript 2.4

完整的破坏性改动列表请到这里查看:breaking change issues

弱类型检测

TypeScript 2.4引入了“弱类型(weak type)”的概念。 若一个类型只包含可选的属性,那么它就被认为是_弱(weak)_的。 例如,下面的Options类型就是一个弱类型:

interface Options {
    data?: string,
    timeout?: number,
    maxRetries?: number,
}

TypeScript 2.4,当给一个弱类型赋值,但是它们之前没有共同的属性,那么就会报错。 例如:

function sendMessage(options: Options) {
    // ...
}

const opts = {
    payload: "hello world!",
    retryOnFail: true,
}

// 错误!
sendMessage(opts);
// 'opts'与'Options'之间没有共同的属性
// 你是否想用'data'/'maxRetries'来替换'payload'/'retryOnFail'

推荐做法

  1. 仅声明那些确定存在的属性。
  2. 给弱类型添加索引签名(如:[propName: string]: {}
  3. 使用类型断言(如:opts as Options

推断返回值的类型

TypeScript现在可从上下文类型中推断出一个调用的返回值类型。 这意味着一些代码现在会适当地报错。 下面是一个例子:

let x: Promise<string> = new Promise(resolve => {
    resolve(10);
    //      ~~ 错误! 'number'类型不能赋值给'string'类型
});

更严格的回调函数参数变化

TypeScript对回调函数参数的检测将与立即签名检测协变。 之前是双变的,这会导致有时候错误的类型也能通过检测。 根本上讲,这意味着回调函数参数和包含回调的类会被更细致地检查,因此Typescript会要求更严格的类型。 这在Promises和Observables上是十分明显的。

Promises

下面是改进后的Promise检查的例子:

let p = new Promise((c, e) => { c(12) });
let u: Promise<number> = p;
    ~
    类型 'Promise<{}>' 不能赋值给 'Promise<number>'

TypeScript无法在调用new Promise时推断类型参数T的值。 因此,它仅推断为Promise<{}>。 不幸的是,它会允许你这样写c(12)c('foo'),就算p的声明明确指出它应该是Promise<number>

在新的规则下,Promise<{}>不能够赋值给Promise<number>,因为它破坏了Promise的回调函数。 TypeScript仍无法推断类型参数,所以你只能通过传递类型参数来解决这个问题:

let p: Promise<number> = new Promise<number>((c, e) => { c(12) });
//                                  ^^^^^^^^ 明确的类型参数

它能够帮助从promise代码体里发现错误。 现在,如果你错误地调用c('foo'),你就会得到一个错误提示:

let p: Promise<number> = new Promise<number>((c, e) => { c('foo') });
//                                                         ~~~~~
//  参数类型 '"foo"' 不能赋值给 'number'

(嵌套)回调

其它类型的回调也会被这个改进所影响,其中主要是嵌套的回调。 下面是一个接收回调函数的函数,回调函数又接收嵌套的回调。 嵌套的回调现在会以协变的方式检查。

declare function f(
  callback: (nested: (error: number, result: any) => void, index: number) => void
): void;

f((nested: (error: number) => void) => { log(error) });
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'(error: number) => void' 不能赋值给 '(error: number, result: any) => void'

修复这个问题很容易。给嵌套的回调传入缺失的参数:

f((nested: (error: number, result: any) => void) => { });

更严格的泛型函数检查

TypeScript在比较两个单一签名的类型时会尝试统一类型参数。 结果就是,当关系到两个泛型签名时检查变得更严格了,但同时也会捕获一些bug。

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

function f(a: A, b: B) {
    a = b;  // Error
    b = a;  // Ok
}

推荐做法

或者修改定义或者使用--noStrictGenericChecks

从上下文类型中推荐类型参数

在TypeScript之前,下面例子中

let f: <T>(x: T) => T = y => y;

y的类型将是any。 这意味着,程序虽会进行类型检查,但是你可以在y上做任何事,比如:

let f: <T>(x: T) => T = y => y() + y.foo.bar;

推荐做法:

适当地重新审视你的泛型是否为正确的约束。实在不行,就为参数加上any注解。

TypeScript 2.3

完整的破坏性改动列表请到这里查看:breaking change issues.

空的泛型列表会被标记为错误

示例

class X<> {}  // Error: Type parameter list cannot be empty.
function f<>() {}  // Error: Type parameter list cannot be empty.
const x: X<> = new X<>();  // Error: Type parameter list cannot be empty.

TypeScript 2.2

完整的破坏性改动列表请到这里查看:breaking change issues.

标准库里的DOM API变动

  • 现在标准库里有Window.fetch的声明;仍依赖于@types\whatwg-fetch会产生声明冲突错误,需要被移除。
  • 现在标准库里有ServiceWorker的声明;仍依赖于@types\service_worker_api会产生声明冲突错误,需要被移除。

TypeScript 2.1

完整的破坏性改动列表请到这里查看:breaking change issues.

生成的构造函数代码将this的值替换为super(...)调用的返回值

在ES2015中,如果构造函数返回一个对象,那么对于任何super(...)的调用者将隐式地替换掉this的值。 因此,有必要获取任何可能的super(...)的返回值并用this进行替换。

示例

定义一个类C

class C extends B {
    public a: number;
    constructor() {
        super();
        this.a = 0;
    }
}

将生成如下代码:

var C = (function (_super) {
    __extends(C, _super);
    function C() {
        var _this = _super.call(this) || this;
        _this.a = 0;
        return _this;
    }
    return C;
}(B));

注意:

  • _super.call(this)存入局部变量_this
  • 构造函数体里所有使用this的地方都被替换为super调用的返回值(例如_this
  • 每个构造函数将明确地返回它的this,以确保正确的继承

值得注意的是在super(...)调用前就使用thisTypeScript 1.8开始将会引发错误。

继承内置类型如ErrorArrayMap将是无效的

做为将this的值替换为super(...)调用返回值的一部分,子类化ErrorArray等的结果可以是非预料的。 这是因为ErrorArray等的构造函数会使用ECMAScript 6的new.target来调整它们的原型链; 然而,在ECMAScript 5中调用构造函数时却没有有效的方法来确保new.target的值。 在默认情况下,其它低级别的编译器也普遍存在这个限制。

示例

针对如下的子类:

class FooError extends Error {
    constructor(m: string) {
        super(m);
    }
    sayHello() {
        return "hello " + this.message;
    }
}

你会发现:

  • 由这个子类构造出来的对象上的方法可能为undefined,因此调用sayHello会引发错误。
  • instanceof应用于子类与其实例之前会失效,因此(new FooError()) instanceof FooError会返回false

推荐

做为一个推荐,你可以在任何super(...)调用后立即手动地调整原型。

class FooError extends Error {
    constructor(m: string) {
        super(m);

        // Set the prototype explicitly.
        Object.setPrototypeOf(this, FooError.prototype);
    }

    sayHello() {
        return "hello " + this.message;
    }
}

但是,任何FooError的子类也必须要手动地设置原型。 对于那些不支持Object.setPrototypeOf的运行时环境,你可以使用__proto__

不幸的是,[这些变通方法在IE10及其之前的版本](https://msdn.microsoft.com/en-us/library/s4esdbwz(v=vs.94).aspx) 你可以手动地将方法从原型上拷贝到实例上(比如从FooError.prototypethis),但是原型链却是无法修复的。

const变量和readonly属性会默认地推断成字面类型

默认情况下,const声明和readonly属性不会被推断成字符串,数字,布尔和枚举字面量类型。这意味着你的变量/属性可能具有比之前更细的类型。这将体现在使用===!==的时候。

示例

const DEBUG = true; // 现在为`true`类型,之前为`boolean`类型

if (DEBUG === false) { // 错误: 操作符'==='不能应用于'true'和'false'
    ...
}

推荐

针对故意要求更加宽泛类型的情况下,将类型转换成基础类型:

const DEBUG = <boolean>true; // `boolean`类型

不对函数和类表达式里捕获的变量进行类型细化

当泛型类型参数具有stringnumberboolean约束时,会被推断为字符串,数字和布尔字面量类型。此外,如果字面量类型有相同的基础类型(如string),当没有字面量类型做为推断的最佳超类型时这个规则会失效。

示例

declare function push<T extends string>(...args: T[]): T;

var x = push("A", "B", "C"); // 推断成 "A" | "B" | "C" 在TS 2.1, 在TS 2.0里为 string

推荐

在调用处明确指定参数类型:

var x = push<string>("A", "B", "C"); // x是string

没有注解的callback参数如果没有与之匹配的重载参数会触发implicit-any错误

在之前编译器默默地赋予callback(下面的c)的参数一个any类型。原因关乎到编译器如何解析重载的函数表达式。从TypeScript 2.1开始,在使用--noImplicitAny时,这会触发一个错误。

示例

declare function func(callback: () => void): any;
declare function func(callback: (arg: number) => void): any;

func(c => { });

推荐

删除第一个重载,因为它实在没什么意义;上面的函数可以使用1个或0个必须参数调用,因为函数可以安全地忽略额外的参数。

declare function func(callback: (arg: number) => void): any;

func(c => { });
func(() => { });

或者,你可以给callback的参数指定一个明确的类型:

func((c:number) => { });

逗号操作符使用在无副作用的表达式里时会被标记成错误

大多数情况下,这种在之前是有效的逗号表达式现在是错误。

示例

let x = Math.pow((3, 5)); // x = NaN, was meant to be `Math.pow(3, 5)`

// This code does not do what it appears to!
let arr = [];
switch(arr.length) {
  case 0, 1:
    return 'zero or one';
  default:
    return 'more than one';
}

推荐

--allowUnreachableCode会禁用产生警告在整个编译过程中。或者,你可以使用void操作符来镇压这个逗号表达式错误:

let a = 0;
let y = (void a, 1); // no warning for `a`

标准库里的DOM API变动

  • Node.firstChildNode.lastChildNode.nextSiblingNode.previousSiblingNode.parentElementNode.parentNode现在是Node | null而非Node

    查看#11113了解详细信息。

    推荐明确检查null或使用!断言操作符(比如node.lastChild!)。

TypeScript 2.0

完整的破坏性改动列表请到这里查看:breaking change issues.

对函数或类表达式的捕获变量不进行类型细化(narrowing)

类型细化不会在函数,类和lambda表达式上进行。

例子

var x: number | string;

if (typeof x === "number") {
    function inner(): number {
        return x; // Error, type of x is not narrowed, c is number | string
    }
    var y: number = x; // OK, x is number
}

编译器不知道回调函数什么时候被执行。考虑下面的情况:

var x: number | string = "a";
if (typeof x === "string") {
    setTimeout(() => console.log(x.charAt(0)), 0);
}
x = 5;

x.charAt()被调用的时候把x的类型当作string是错误的,事实上它确实不是string类型。

推荐

使用常量代替:

const x: number | string = "a";
if (typeof x === "string") {
    setTimeout(() => console.log(x.charAt(0)), 0);
}

泛型参数会进行类型细化

例子

function g<T>(obj: T) {
    var t: T;
    if (obj instanceof RegExp) {
         t = obj; // RegExp is not assignable to T
    }
}

推荐 可以把局部变量声明为特定类型而不是泛型参数或者使用类型断言。

只有get而没有set的存取器会被自动推断为readonly属性

例子

class C {
  get x() { return 0; }
}

var c = new C();
c.x = 1; // Error Left-hand side is a readonly property

推荐

定义一个不对属性写值的setter。

在严格模式下函数声明不允许出现在块(block)里

在严格模式下这已经是一个运行时错误。从TypeScript 2.0开始,它会被标记为编译时错误。

例子

if( true ) {
    function foo() {}
}

export = foo;

推荐

使用函数表达式代替:

if( true ) {
    const foo = function() {}
}

TemplateStringsArray现是是不可变的

ES2015模版字符串总是将它们的标签以不可变的类数组对象进行传递,这个对象带有一个raw属性(同样是不可变的)。 TypeScript把这个对象命名为TemplateStringsArray

便利的是,TemplateStringsArray可以赋值给Array<string>,因此你可以利用这个较短的类型来使用标签参数:

function myTemplateTag(strs: string[]) {
    // ...
}

然而,在TypeScript 2.0,支持用readonly修饰符表示这些对象是不可变的。 这样的话,TemplateStringsArray 就变成了不可变的,并且不再可以赋值给string[]

推荐

直接使用TemplateStringsArray(或者使用ReadonlyArray<string>)。

TypeScript 1.8

完整的破坏性改动列表请到这里查看:breaking change issues

现在生成模块代码时会带有"use strict";

在ES6模式下模块总是在严格模式下解析,对于生成目标为非ES6的却不是这样。从TypeScript 1.8开始,生成的模块将总为严格模式。这应该不会对现有的大部分代码产生影响,因为TypeScript把大多数因为严格模式而产生的错误当做编译时错误,但还是有一些在运行时才发生错误的TypeScript代码,比如赋值给NaN,现在将会直接报错。你可以参考MDN Article学习关于严格模式与非严格模式的区别。

若想禁用这个行为,在命令行里传--noImplicitUseStrict选项或在tsconfig.json文件里指定。

从模块里导出非局部名称

依据ES6/ES2015规范,从模块里导出非局部名称将会报错。

例子

export { Promise }; // Error

推荐

在导出之前,使用局部变量声明捕获那个全局名称。

const localPromise = Promise;
export { localPromise as Promise };

默认启用代码可达性(Reachability)检查

TypeScript 1.8里,我们添加了一些可达性检查来阻止一些种类的错误。特别是:

  1. 检查代码的可达性(默认启用,可以通过allowUnreachableCode编译器选项禁用)

       function test1() {
           return 1;
           return 2; // error here
       }
    
       function test2(x) {
           if (x) {
               return 1;
           }
           else {
               throw new Error("NYI")
           }
           var y = 1; // error here
       }
    
  2. 检查标签是否被使用(默认启用,可以通过allowUnusedLabels编译器选项禁用)

    l: // error will be reported - label `l` is unused
    while (true) {
    }
    
    (x) => { x:x } // error will be reported - label `x` is unused
    
  3. 检查是否函数里所有带有返回值类型注解的代码路径都返回了值(默认启用,可以通过noImplicitReturns编译器选项禁用)

    // error will be reported since function does not return anything explicitly when `x` is falsy.
    function test(x): number {
       if (x) return 10;
    }
    
  4. 检查控制流是否能进到switch语句的case里(默认禁用,可以通过noFallthroughCasesInSwitch编译器选项启用)。注意没有语句的case不会被检查。

    switch(x) {
       // OK
       case 1:
       case 2:
           return 1;
    }
    switch(x) {
       case 1:
           if (y) return 1;
       case 2:
           return 2;
    }
    

如果你看到了这些错误,但是你认为这时的代码是合理的话,你可以通过编译选项来阻止报错。

--module不允许与--outFile一起出现,除非 --module被指定为amdsystem

之前使用模块指定这两个的时候,会生成空的out文件且不会报错。

标准库里的DOM API变动

  • ImageData.data现在的类型为Uint8ClampedArray而不是number[]。查看#949
  • HTMLSelectElement .options现在的类型为HTMLCollection而不是HTMLSelectElement。查看#1558
  • HTMLTableElement.createCaptionHTMLTableElement.createTBodyHTMLTableElement.createTFootHTMLTableElement.createTHeadHTMLTableElement.insertRowHTMLTableSectionElement.insertRowHTMLTableElement.insertRow现在返回HTMLTableRowElement而不是HTMLElement。查看#3583
  • HTMLTableRowElement.insertCell现在返回HTMLTableCellElement而不是HTMLElement查看#3583
  • IDBObjectStore.createIndexIDBDatabase.createIndex第二个参数类型为IDBObjectStoreParameters而不是any。查看#5932
  • DataTransferItemList.Item返回值类型变为DataTransferItem而不是File。查看#6106
  • Window.open返回值类型变为Window而不是any。查看#6418
  • WeakMap.clear被移除。查看#6500

在super-call之前不允许使用this

ES6不允许在构造函数声明里访问this

比如:

class B {
    constructor(that?: any) {}
}

class C extends B {
    constructor() {
        super(this);  // error;
    }
}

class D extends B {
    private _prop1: number;
    constructor() {
        this._prop1 = 10;  // error
        super();
    }
}

TypeScript 1.7

完整的破坏性改动列表请到这里查看:breaking change issues

this中推断类型发生了变化

在类里,this值的类型将被推断成this类型。 这意味着随后使用原始类型赋值时可能会发生错误。

例子:

class Fighter {
    /** @returns the winner of the fight. */
    fight(opponent: Fighter) {
        let theVeryBest = this;
        if (Math.rand() < 0.5) {
            theVeryBest = opponent; // error
        }
        return theVeryBest
    }
}

推荐:

添加类型注解:

class Fighter {
    /** @returns the winner of the fight. */
    fight(opponent: Fighter) {
        let theVeryBest: Fighter = this;
        if (Math.rand() < 0.5) {
            theVeryBest = opponent; // no error
        }
        return theVeryBest
    }
}

类成员修饰符后面会自动插入分号

关键字abstract,public,protectedprivate是ECMAScript 3里的_保留关键字_并适用于自动插入分号机制。 之前,在这些关键字出现的行尾,TypeScript是不会插入分号的。 现在,这已经被改正了,在上例中abstract class D不再能够正确地继承C了,而是声明了一个m方法和一个额外的属性abstract

注意,asyncdeclare已经能够正确自动插入分号了。

例子:

abstract class C {
    abstract m(): number;
}
abstract class D extends C {
    abstract
    m(): number;
}

推荐:

在定义类成员时删除关键字后面的换行。通常来讲,要避免依赖于自动插入分号机制。

TypeScript 1.6

完整的破坏性改动列表请到这里查看:breaking change issues

严格的对象字面量赋值检查

当在给变量赋值或给非空类型的参数赋值时,如果对象字面量里指定的某属性不存在于目标类型中时会得到一个错误。

你可以通过使用--suppressExcessPropertyErrors编译器选项来禁用这个新的严格检查。

例子:

var x: { foo: number };
x = { foo: 1, baz: 2 };  // Error, excess property `baz`

var y: { foo: number, bar?: number };
y = { foo: 1, baz: 2 };  // Error, excess or misspelled property `baz`

推荐:

为了避免此错误,不同情况下有不同的补救方法:

如果目标类型接收额外的属性,可以增加一个索引:

var x: { foo: number, [x: string]: any };
x = { foo: 1, baz: 2 };  // OK, `baz` matched by index signature

如果原始类型是一组相关联的类型,使用联合类型明确指定它们的类型而不是仅指定一个基本类型。

let animalList: (Dog | Cat | Turkey)[] = [    // use union type instead of Animal
    {name: "Milo", meow: true },
    {name: "Pepper", bark: true},
    {name: "koko", gobble: true}
];

还有可以明确地转换到目标类型以避免此错误:

interface Foo {
    foo: number;
}
interface FooBar {
    foo: number;
    bar: number;
}
var y: Foo;
y = <FooBar>{ foo: 1, bar: 2 };

CommonJS的模块解析不再假设路径为相对的

之前,对于one.tstwo.ts文件,如果它们在相同目录里,那么在two.ts里面导入"one"时是相对于one.ts的路径的。

TypeScript 1.6在编译CommonJS时,"one"不再等同于"./one"。取而代之的是会相对于合适的node_modules文件夹进行查找,与Node.js在运行时解析模块相似。更多详情,阅读the issue that describes the resolution algorithm

例子:

./one.ts

export function f() {
    return 10;
}

./two.ts

import { f as g } from "one";

推荐:

修改所有计划之外的非相对的导入。

./one.ts

export function f() {
    return 10;
}

./two.ts

import { f as g } from "./one";

--moduleResolution编译器选项设置为classic

函数和类声明为默认导出时不再能够与在意义上有交叉的同名实体进行合并

在同一空间内默认导出声明的名字与空间内一实体名相同时会得到一个错误;比如,

export default function foo() {
}

namespace foo {
    var x = 100;
}

export default class Foo {
    a: number;
}

interface Foo {
    b: string;
}

两者都会报错。

然而,在下面的例子里合并是被允许的,因为命名空间并不具备做为值的意义:

export default class Foo {
}

namespace Foo {
}

推荐:

为默认导出声明本地变量并使用单独的export default语句:

class Foo {
    a: number;
}

interface foo {
    b: string;
}

export default Foo;

更多详情,请阅读the originating issue

模块体以严格模式解析

按照ES6规范,模块体现在以严格模式进行解析。行为将相当于在模块作用域顶端定义了"use strict";它包括限制了把argumentseval做为变量名或参数名的使用,把未来保留字做为变量或参数使用,八进制数字字面量的使用等。

标准库里DOM API的改动

  • MessageEventProgressEvent构造函数希望传入参数;查看issue #4295
  • ImageData构造函数希望传入参数;查看issue #4220
  • File构造函数希望传入参数;查看issue #3999

系统模块输出使用批量导出

编译器以系统模块的格式使用新的_export函数批量导出的变体,它接收任何包含键值对的对象做为参数而不是key, value。

模块加载器需要升级到v0.17.1或更高。

npm包的.js内容从'bin'移到了'lib'

TypeScript的npm包入口位置从bin移动到了lib,以防‘node_modules/typescript/bin/typescript.js’通过IIS访问的时候造成阻塞(bin默认是隐藏段因此IIS会阻止访问这个文件夹)。

TypeScript的npm包不会默认全局安装

TypeScript 1.6从package.json里移除了preferGlobal标记。如果你依赖于这种行为,请使用npm install -g typescript

装饰器做为调用表达式进行检查

从1.6开始,装饰器类型检查更准确了;编译器会将装饰器表达式做为以被装饰的实体做为参数的调用表达式来进行检查。这可能会造成以前的代码报错。

TypeScript 1.5

完整的破坏性改动列表请到这里查看:breaking change issues

不允许在箭头函数里引用arguments

这是为了遵循ES6箭头函数的语义。之前箭头函数里的arguments会绑定到箭头函数的参数。参照ES6规范草稿 9.2.12,箭头函数不存在arguments对象。 从TypeScript 1.5开始,在箭头函数里使用arguments会被标记成错误以确保你的代码转成ES6时没语义上的错误。

例子:

function f() {
    return () => arguments; // Error: The 'arguments' object cannot be referenced in an arrow function.
}

推荐:

// 1. 使用带名字的剩余参数
function f() {
    return (...args) => { args; }
}

// 2. 使用函数表达式
function f() {
    return function(){ arguments; }
}

内联枚举引用的改动

对于正常的枚举,在1.5之前,编译器_仅会_内联常量成员,且成员仅在使用字面量初始化时才被当做是常量。这在判断检举值是使用字面量初始化还是表达式时会行为不一致。从TypeScript 1.5开始,所有非const枚举成员都不会被内联。

例子:

var x = E.a;  // previously inlined as "var x = 1; /*E.a*/"

enum E {
   a = 1
}

推荐: 在枚举声明里添加const修饰符来确保它总是被内联。 更多信息,查看#2183

上下文的类型将作用于super和括号表达式

在1.5之前,上下文的类型不会作用于括号表达式内部。这就要求做显示的类型转换,尤其是在_必须_使用括号来进行表达式转换的场合。

在下面的例子里,m具有上下文的类型,它在之前的版本里是没有的。

var x: SomeType = (n) => ((m) => q);
var y: SomeType = t ? (m => m.length) : undefined;

class C extends CBase<string> {
    constructor() {
        super({
            method(m) { return m.length; }
        });
    }
}

更多信息,查看#1425#920

DOM接口的改动

TypeScript 1.5改进了lib.d.ts库里的DOM类型。这是自TypeScript 1.0以来第一次大的改动;为了拥抱标准DOM规范,很多特定于IE的定义被移除了,同时添加了新的类型如Web Audio和触摸事件。

变通方案:

你可以使用旧的lib.d.ts配合新版本的编译器。你需要在你的工程里引入之前版本的一个拷贝。这里是本次改动之前的lib.d.ts文件(TypeScript 1.5-alpha)

变动列表:

  • 属性selectionDocument类型上移除
  • 属性clipboardDataWindow类型上移除
  • 删除接口MSEventAttachmentTarget
  • 属性onresizedisableduniqueIDremoveNodefireEventcurrentStyleruntimeStyleHTMLElement类型上移除
  • 属性urlEvent类型上移除
  • 属性execScriptnavigateitemWindow类型上移除
  • 属性documentModeparentWindowcreateEventObjectDocument类型上移除
  • 属性parentWindowHTMLDocument类型上移除
  • 属性setCapture被完全移除
  • 属性releaseCapture被完全移除
  • 属性setAttributestyleFloatpixelLeftCSSStyleDeclaration类型上移除
  • 属性selectorTextCSSRule类型上移除
  • CSSStyleSheet.rules现在是CSSRuleList类型,而非MSCSSRuleList
  • documentElement现在是Element类型,而非HTMLElement
  • Event具有一个新的必需属性returnValue
  • Node具有一个新的必需属性baseURI
  • Element具有一个新的必需属性classList
  • Location具有一个新的必需属性origin
  • 属性MSPOINTER_TYPE_MOUSEMSPOINTER_TYPE_TOUCHMSPointerEvent类型上移除
  • CSSStyleRule具有一个新的必需属性readonly
  • 属性execUnsafeLocalFunctionMSApp类型上移除
  • 全局方法toStaticHTML被移除
  • HTMLCanvasElement.getContext现在返回CanvasRenderingContext2D | WebGLRenderingContex
  • 移除扩展类型DataviewWeakmapMapSet
  • XMLHttpRequest.send具有两个重载send(data?: Document): void;send(data?: String): void;
  • window.orientation现在是string类型,而非number
  • 特定于IE的attachEventdetachEventWindow上移除

以下是被新加的DOM类型所部分或全部取代的代码库的代表:

  • DefinitelyTyped/auth0/auth0.d.ts
  • DefinitelyTyped/gamepad/gamepad.d.ts
  • DefinitelyTyped/interactjs/interact.d.ts
  • DefinitelyTyped/webaudioapi/waa.d.ts
  • DefinitelyTyped/webcrypto/WebCrypto.d.ts

更多信息,查看完整改动

类代码体将以严格格式解析

按照ES6规范,类代码体现在以严格模式进行解析。行为将相当于在类作用域顶端定义了"use strict";它包括限制了把argumentseval做为变量名或参数名的使用,把未来保留字做为变量或参数使用,八进制数字字面量的使用等。

TypeScript 1.4

完整的破坏性改动列表请到这里查看:breaking change issues

阅读issue #868以了解更多关于联合类型的破坏性改动。

多个最佳通用类型候选

当有多个最佳通用类型可用时,现在编译器会做出选择(依据编译器的具体实现)而不是直接使用第一个。

var a: { x: number; y?: number };
var b: { x: number; z?: number };

// 之前 { x: number; z?: number; }[]
// 现在 { x: number; y?: number; }[]
var bs = [b, a];

这会在多种情况下发生。具有一组共享的必需属性和一组其它互斥的(可选或其它)属性,空类型,兼容的签名类型(包括泛型和非泛型签名,当类型参数上应用了any时)。

推荐 使用类型注解指定你要使用的类型。

var bs: { x: number; y?: number; z?: number }[] = [b, a];

泛型接口

当在多个T类型的参数上使用了不同的类型时会得到一个错误,就算是添加约束也不行:

declare function foo<T>(x: T, y:T): T;
var r = foo(1, ""); // r used to be {}, now this is an error

添加约束:

interface Animal { x }
interface Giraffe extends Animal { y }
interface Elephant extends Animal { z }
function f<T extends Animal>(x: T, y: T): T { return undefined; }
var g: Giraffe;
var e: Elephant;
f(g, e);

在这里查看详细解释

推荐 如果这种不匹配的行为是故意为之,那么明确指定类型参数:

var r = foo<{}>(1, ""); // Emulates 1.0 behavior
var r = foo<string|number>(1, ""); // Most useful
var r = foo<any>(1, ""); // Easiest
f<Animal>(g, e);

_或_重写函数定义指明就算不匹配也没问题:

declare function foo<T,U>(x: T, y:U): T|U;
function f<T extends Animal, U extends Animal>(x: T, y: U): T|U { return undefined; }

泛型剩余参数

不能再使用混杂的参数类型:

function makeArray<T>(...items: T[]): T[] { return items; }
var r = makeArray(1, ""); // used to return {}[], now an error

new Array(...)也一样

推荐 声明向后兼容的签名,如果1.0的行为是你想要的:

function makeArray<T>(...items: T[]): T[];
function makeArray(...items: {}[]): {}[];
function makeArray<T>(...items: T[]): T[] { return items; }

带类型参数接口的重载解析

var f10: <T>(x: T, b: () => (a: T) => void, y: T) => T;
var r9 = f10('', () => (a => a.foo), 1); // r9 was any, now this is an error

推荐 手动指定一个类型参数

var r9 = f10<any>('', () => (a => a.foo), 1);

类声明与类型表达式以严格模式解析

ECMAScript 2015语言规范(ECMA-262 6th Edition)指明_ClassDeclaration_和_ClassExpression_使用严格模式。 因此,在解析类声明或类表达式时将使用额外的限制。

例如:

class implements {}  // Invalid: implements is a reserved word in strict mode
class C {
    foo(arguments: any) {   // Invalid: "arguments" is not allow as a function argument
        var eval = 10;      // Invalid: "eval" is not allowed as the left-hand-side expression
        arguments = [];     // Invalid: arguments object is immutable
    }
}

关于严格模式限制的完整列表,请阅读 Annex C - The Strict Mode of ECMAScript of ECMA-262 6th Edition。