编译选项

编译选项是传递给编译器程序的参数,使用编译选项能够改变编译器的默认行为。在编译程序时,编译选项不是必须指定的。

本节不会介绍完整的编译选项列表,而是会列举出部分常用的编译选项并介绍如何使用它们。如果读者想要了解最新的完整的编译选项列表,建议去查阅官方文档。在本节中,我们会着重介绍严格类型检查编译选项,因为这些编译选项能够帮助用户提高代码质量。

编译选项风格

TypeScript 编译选项的命名风格包含以下两种:

  • 长名字风格,如 --help

  • 短名字风格,如 -h

每一个编译选项都有一个长名字,但是不一定有短名字。在 TypeScript 中,不论是长名字风格的编译选项还是短名字风格的编译选项均不区分大小写,即 --help--HELP-h-H 表示相同的含义。

长名字风格的编译选项由两个连字符和一个单词词组构成。提供长名字风格的命令行选项是推荐的做法。它有助于在不同程序之间保持一致的、具有描述性的选项名,从而提高开发者的使用体验。

短名字风格的编译选项名由单个连字符和单个字母构成。TypeScript 编译器仅针对一小部分常用的编译选项提供了短名字。如果一个编译选项具有短名字形式,那么该短名字通常为其长名字的首字母,例如 --help 编译选项的短名字为 -h

由于提供了短名字的编译选项数量较少且十分常用,因此我们在表 8-1 中列出了所有支持短名字风格的编译选项。

image 2024 02 07 19 10 45 664
Figure 1. 表8-1 支持短名字风格的编译选项

使用编译选项

在运行 tsc 命令时,可以在命令行上指定编译选项。有一些编译选项在使用时不必传入参数,只需要写出编译选项名即可,例如 --version。示例如下:

tsc --version

我们也可以使用 --version 编译选项的短名字形式 -v。示例如下:

tsc -v

实际上,每一个编译选项都能够接受一个参数值,只不过有一些编译选项具有默认值,因此也可以省略传入参数。在给编译选项传入参数时,需要将参数写在编译选项名之后,并以空格字符分隔。例如,--emitBOM 编译选项接受 truefalse 作为参数值。该编译选项设置了编译器在生成输出文件时是否插入 byte order mark(BOM)。示例如下:

tsc --emitBOM true

如果编译选项的参数值是布尔类型并且值为 true,那么就可以省略传入参数值。因此,上例中的命令等同于:

tsc --emitBOM

但如果编译选项的参数值不是布尔类型的 truefalse,那么就不能省略参数值,必须在命令行上设置一个参数值。示例如下:

tsc --target ES5

如果想要同时使用多个编译选项,那么在编译选项之间使用空格分隔即可。示例如下:

tsc --version --locale zh-CN

此例中,同时使用了 --version--locale 编译选项。--locale 编译选项能够设置显示信息时使用的区域和语言,它的可选值如下:

  • 英语:en

  • 捷克语:cs

  • 德语:de

  • 西班牙语:es

  • 法语:fr

  • 意大利语:it

  • 日语:ja

  • 韩语:ko

  • 波兰语:pl

  • 葡萄牙语:pt-BR

  • 俄语:ru

  • 土耳其语:tr

  • 简体中文:zh-CN

严格类型检查

TypeScript 编译器提供了两种类型检查模式,即严格类型检查和非严格类型检查。

非严格类型检查是默认的类型检查模式,该模式下的类型检查比较宽松。在将已有的 JavaScript 代码迁移到 TypeScript 时,通常会使用这种类型检查模式,因为这样做可以让迁移工作更加顺利地进行,不至于一时产生过多的错误。

在严格类型检查模式下,编译器会进行额外的类型检查,从而能够更好地保证程序的正确性。严格类型检查功能使用一系列编译选项来开启。在开始一个新的工程时,强烈推荐启用所有严格检查编译选项。对于已有的工程,则可以逐步启用这些编译选项。因为只有如此,才能够最大限度地利用编译器的静态类型检查功能。

关于类型检查的详细介绍请参考 5.2 节。

--strict

--strict 编译选项是所有严格类型检查编译选项的 总开关。如果启用了 --strict 编译选项,那么就相当于同时启用了下列编译选项:

  • --noImplicitAny

  • --strictNullChecks

  • --strictFunctionTypes

  • --strictBindCallApply

  • --strictPropertyInitialization

  • --noImplicitThis

  • --alwaysStrict

在实际工程中,我们可以先启用 --strict 编译选项,然后再根据需求禁用不需要的某些严格类型检查编译选项。这样做有一个优点,那就是在 TypeScript 语言发布新版本时可能会引入新的严格类型检查编译选项,如果启用了 --strict 编译选项,那么就会自动应用新引入的严格类型检查编译选项。

--strict 编译选项既可以在命令行上使用,也可以在 tsconfig.json 配置文件中使用。

在命令行上使用该编译选项,示例如下:

tsc --strict

tsconfig.json 配置文件中使用该编译选项,示例如下:

{
    "compilerOptions": {
        "strict": true
    }
}

上例的配置等同于如下 tsconfig.json 配置文件:

{
    "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictBindCallApply": true,
        "strictPropertyInitialization": true,
        "noImplicitThis": true,
        "alwaysStrict": true
    }
}

--noImplicitAny

若一个表达式没有明确的类型注解并且编译器又无法推断出一个具体的类型时,那么它将被视为 any 类型。编译器不会对 any 类型进行类型检查,因此可能存在潜在的错误。

例如,下例中的函数参数 str 既没有类型注解也无法推断出具体类型,因此它的类型为 any 类型。不论我们使用哪种类型调用函数 f 都不会产生编译错误,但如果实际参数不是 string 类型,那么在代码运行时会产生错误。示例如下:

/**
 * --noImplicitAny=false
 */
function f(str) {
    //     ~~~
    //     类型为:any

    console.log(str.substring(3));
}

f(42); // 运行时错误

如果启用了 --noImplicitAny 编译选项,那么当表达式的推断类型为 any 类型时将产生编译错误。因此,上例中的代码在启用了 --noImplicitAny 编译选项的情况下将产生编译错误。示例如下:

/**
 * --noImplicitAny=true
 */
function f(str) {
    //     ~~~
    //     编译错误!参数 'str' 隐式地成为 'any' 类型

    console.log(str.substring(3));
}

f(42);

关于该编译选项的详细介绍请参考 5.7.1 节。

--strictNullChecks

若没有启用 --strictNullChecks 编译选项,编译器在类型检查时将忽略 undefined 值和 null 值。示例如下:

/**
 * --strictNullChecks=false
 */
function f(str: string) {
    console.log(str.substring(3));
}

// 以下均没有编译错误,但在运行时产生错误
f(undefined);
f(null);

此例中,函数 f 期望传入 string 类型的参数,并且在传入 undefined 值和 null 值时,编译器没有产生错误。因为在没有启用 --strictNullChecks 编译选项的情况下,当编译器遇到 undefined 值和 null 值时会跳过类型检查。而实际上,此例中的代码在运行时会产生错误,因为在 undefined 值或 null 值上调用方法将抛出 TypeError 异常。

如果启用了 --strictNullChecks 编译选项,那么 undefined 值只能赋值给 undefined 类型(顶端类型、void 类型除外),null 值也只能赋值给 null 类型(顶端类型除外),两者都明确地拥有了各自的类型。因此,上例中的代码在该编译选项下会产生编译错误。示例如下:

/**
 * --strictNullChecks=true
 */
function f(str: string) {
    console.log(str.substring(3));
}

f(undefined);
//~~~~~~~~~
//编译错误!'undefined' 不能赋值给 'string' 类型的参数

f(null);
//~~~~
//编译错误!'null' 不能赋值给 'string' 类型的参数

关于该编译选项的详细介绍请参考 5.3.6 节。

--strictFunctionTypes

该编译选项用于配置编译器对函数类型的类型检查规则。

如果启用了 --strictFunctionTypes 编译选项,那么函数参数类型与函数类型之间是逆变关系。

如果禁用了 --strictFunctionTypes 编译选项,那么函数参数类型与函数类型之间是相对宽松的双变关系。

不论是否启用了 --strictFunctionTypes 编译选项,函数返回值类型与函数类型之间始终是协变关系。

关于该编译选项的详细介绍请参考 7.1.5 节。

--strictBindCallApply

Function.prototype.callFunction.prototype.bindFunction.prototype.applyJavaScript 语言中函数对象上的内置方法。这三个方法都能够绑定函数调用时的 this 值。例如,下例中的 6、7、8 行将调用函数 f 时的 this 值设置成了对象 { name: 'ts' }

function f(this: { name: string }, x: number, y: number) {
    console.log(this.name);
    console.log(x + y);
}

f.apply({ name: 'ts' }, [1, 2]);
f.call({ name: 'ts' }, 1, 2);
f.bind({ name: 'ts' })(1, 2);

如果没有启用 --strictBindCallApply 编译选项,那么编译器不会对以上三个内置方法进行类型检查。虽然函数声明 f 中定义了 this 的类型以及参数 xy 的类型,但是传入任何类型的实际参数都不会产生编译错误。示例如下:

/**
 * --strictBindCallApply=false
 */
function f(this: { name: string }, x: number, y: number) {
    console.log(this.name);
    console.log(x + y);
}

// 下列语句均没有编译错误
f.apply({}, ['param']);
f.call({}, 'param');
f.bind({})('param');

如果启用了 --strictBindCallApply 编译选项,那么编译器将对以上三个内置方法的 this 类型以及参数类型进行严格的类型检查。示例如下:

/**
 * --strictBindCallApply=true
 */
function f(this: Window, str: string) {
    return this.alert(str);
}

f.call(document, 'foo');
//     ~~~~~~~~
//     编译错误!'document' 类型的值不能赋值给 'window' 类型的参数

f.call(window, false);
//             ~~~~~
//             编译错误!'false' 类型的值不能赋值给 'string' 类型的参数

f.apply(document, ['foo']);
//      ~~~~~~~~
//      编译错误!'document' 类型的值不能赋值给 'window' 类型的参数

f.apply(window, [false]);
//               ~~~~~
//               编译错误!'false' 类型的值不能赋值给 'string' 类型的参数

f.bind(document);
//     ~~~~~~~~
//     编译错误!'document' 类型的值不能赋值给 'window' 类型的参数

// 正确的用法
f.call(window, 'foo');
f.apply(window, ['foo']);
f.bind(window);

--strictPropertyInitialization

该编译选项用于配置编译器对类属性的初始化检查。

如果启用了 --strictPropertyInitialization 编译选项,那么当类的属性没有进行初始化时将产生编译错误。类的属性既可以在声明时直接初始化,例如下例中的属性 x,也可以在构造函数中初始化,例如下例中的属性 y。如果一个属性没有使用这两种方式之一进行初始化,那么会产生编译错误,例如下例中的属性 z。示例如下:

/**
 * -- strictPropertyInitialization=true
 */
class Point {
    x: number = 0;

    y: number;

    z: number;  // 编译错误!属性 'z' 没有初始值,也没有在构造函数中初始化

    constructor() {
        this.y = 0;
    }
}

若没有启用 --strictPropertyInitialization 编译选项,那么上例中的代码不会产生编译错误。也就是说,允许未初始化的属性 z 存在。使用该编译选项时需要注意一种特殊情况,有时候我们会在构造函数中调用其他方法来初始化类的属性,而不是在构造函数中直接进行初始化。目前,编译器无法识别出这种情况,依旧会认为类的属性没有被初始化,进而产生编译错误。我们可以使用 ! 类型断言来解决这个问题,示例如下:

/**
 * -- strictPropertyInitialization=true
 */
class Point {
    x: number;  // 编译错误:属性 'x' 没有初始值,也没有在构造函数中初始化

    y!: number; // 正确

    constructor() {
        this.initX();
        this.initY();
    }

    private initX() {
        this.x = 0;
    }

    private initY() {
        this.y = 0;
    }
}

关于该编译选项的详细介绍请参考 5.15.2 节。

--noImplicitThis

--noImplicitAny 编译选项类似,在启用了 --noImplicitThis 编译选项时,如果程序中的 this 值隐式地获得了 any 类型,那么将产生编译错误。示例如下:

/**
 * -- noImplicitThis=true
 */
class Rectangle {
    width: number;
    height: number;

    constructor(width: number, height: number) {
        this.width = width;
        this.height = height;
    }

    getAreaFunctionWrong() {
        return function () {
            return this.width * this.height;
            //     ~~~~         ~~~~
            //     编译错误:'this' 隐式地获得了 'any' 类型
            //     因为不存在类型注解
        };
    }

    getAreaFunctionCorrect() {
        return function (this: Rectangle) {
            return this.width * this.height;
        };
    }
}

关于该编译选项的详细介绍请参考 5.12.13 节。

--alwaysStrict

ECMAScript 5 引入了一个称为严格模式的新特性。在全局 JavaScript 代码或函数代码的开始处添加 "use strict" 指令就能够启用 JavaScript 严格模式。在模块和类中则会始终启用 JavaScript 严格模式。注意,JavaScript 严格模式不是本节所讲的 TypeScript 严格类型检查模式。

JavaScript 严格模式下,JavaScript 有着更加严格的语法要求和一些新的语义。例如,implementsinterfaceletpackageprivateprotectedpublicstaticyield 都成了保留关键字;在函数的形式参数列表中,不允许出现同名的形式参数等。

若启用了 --alwaysStrict 编译选项,则编译器总是以 JavaScript 严格模式的要求来检查代码,并且在编译生成 JavaScript 代码时会在代码的开始位置添加 "use strict" 指令。示例如下:

/**
 * --alwaysStrict=true
 */
function outer() {
    if (true) {
        function inner() {
            //   ~~~~~
            //   编译错误!当编译目标为'ES3'或'ES5'时,
            //   在严格模式下的语句块中不允许使用函数声明
        }
    }
}

此例中,只有在启用了 --alwaysStrict 编译选项时,第 6 行代码才会产生编译错误。因为在 JavaScript 严格模式下,语句块中不允许出现函数声明。

编译选项列表

随着 TypeScript 版本的更新,提供的编译选项列表也会有所变化。例如,一些编译选项会被废弃,也会有一些新加入的编译选项。推荐读者到 TypeScript 官方网站 上的 Compiler Options 页面中了解最新的编译选项列表。