编译选项:
类型和模块,天哪!
以你的方式编译。
TypeScript 可高度定制,并可适应所有常见的 JavaScript 使用模式。它可以用于从传统浏览器代码到最现代的服务器环境的项目。
TypeScript 的大部分可配置性来自其超过 100 个配置选项,可以通过以下方式之一提供:
- 传递到
tsc
的命令行 (CLI) 标志 - “TSConfig” TypeScript 配置文件
本章不旨在全面介绍所有 TypeScript 配置选项。相反,建议将本章视为你将要使用的最常见选项的介绍。我仅包括了最有用和广泛使用于大多数TypeScript项目设置的选项。有关这些选项以及更多选项的完整参考,请参见aka.ms/tsc。
TSC 选项
回到第 1 章“从 JavaScript 到 TypeScript”,你使用 tsc index.ts
编译 index.ts文件。tsc
命令可以将 TypeScript 的大多数配置选项作为 --
标志。
例如,若要对 index.ts 文件运行 tsc
并跳过生成 index.js文件(因此,仅运行类型检查),请传递 --noEmit
标志:
tsc index.ts --noEmit
你可以运行 tsc --help
来获取常用的命令行选项列表。从 aka.ms/tsc 查看所有 tsc 配置选项的完整列表,可以使用 tsc --all。
美化模式
tsc
CLI 能够以“美化”模式输出:使用颜色和间距进行风格化,使其更易于阅读。如果检测到输出终端支持彩色文本,则默认为美化模式。
下面是 tsc
从文件打印两个类型错误的示例(图 13-1)。
图 13-1。tsc
报告两个错误,包括蓝色文件名、黄线和列号以及红色波浪线
如果你更喜欢更精简和/或没有不同颜色的 CLI 输出,你可以显式提供 --pretty false
来告诉 TypeScript 使用更简洁的无色格式(图 13-2)。
图 13-2。tsc
报告两个纯文本错误
监听模式
我最喜欢的使用 tsc
CLI 的方法是使用 -w/--watch
模式。监视模式不会在完成后退出,而是使 TypeScript 无限期运行,并不断更新你的终端,其中包含它看到的所有错误的实时列表。
在包含两个错误的文件上以监视模式运行(图 13-3)。
图 13-3。tsc
报告在监视模式下抛出两个错误
图 13-4 显示 tsc
更新控制台输出,以指示文件已以修复所有错误的方式进行了更改。
图 13-4。tsc
报告在监视模式下没有错误
监视模式在处理大型更改(如跨多个文件的重构)时特别有用。你可以使用 TypeScript 的类型错误作为各种清单,以查看仍需要清理的内容。
TSConfig 文件
大多数配置选项可以在目录中的 tsconfig.json (“TSConfig”)文件中指定,而不是总是向 tsc
提供所有文件名和配置选项。
tsconfig.json 的存在表明该目录是 TypeScript 项目的根目录。在目录中运行 tsc
将读取该 tsconfig.json 文件中的任何配置选项。
你还可以将 -p/--project
传递给 tsc
,其中包含包含 tsconfig.json 或任何文件的路径,让 tsc
使用它:
tsc -p path/to/tsconfig.json
通常强烈建议尽可能将 TSConfig 文件用于 TypeScript 项目。诸如 VS Code 之类的 IDE 在为你提供智能感知功能时将遵循其配置。
访问 aka.ms/tsconfig.json 获取 TSConfig 文件中可用的配置选项的完整列表。
如果你没有在 tsconfig.json 中设置选项,请不要担心 TypeScript 的默认设置可能会更改并干扰项目的编译设置。这几乎从未发生过,如果发生,则需要对 TypeScript 进行主要版本更新,并在发行说明中注明。
TSC --init
tsc
命令行包含一个 --init
命令,用于创建新的 tsconfig.json 文件。新创建的 TSConfig 文件将包含指向配置文档的链接以及大多数允许的 TypeScript 配置选项,并带有一行注释,简要描述了它们的使用。
运行以下命令:
tsc --init
将生成一个完全注释的 tsconfig.json 文件:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
// ...
}
}
我建议在前几个 TypeScript 项目中使用 tsc --init
创建配置文件。它的默认值适用于大多数项目,其文档注释有助于理解它们。
CLI 与配置
查看由 tsc --init
创建的 TSConfig 文件,你可能会注意到该文件中的配置选项位于一个 "compilerOptions"
对象中。CLI 和 TSConfig 文件中可用的大多数选项都属于以下两个类别之一:
- 编译选项
控制 TypeScript 如何编译和/或检查每个包含的文件进行类型检查。 - 文件选项
指定哪些文件需要或不需要进行 TypeScript 处理。
我们将在这两个类别之后讨论的其他设置,例如项目引用,通常只能在 TSConfig 文件中可用。
如果通过 tsc CLI 提供了某个设置,例如 CI 或生产构建进行一次性更改,通常会覆盖 TSConfig 文件中指定的任何值。由于 IDE 通常从目录中的 tsconfig.json 读取 TypeScript 设置,因此建议将大多数配置选项放在 tsconfig.json 文件中。
文件包含
默认情况下,tsc
将在当前目录和所有子目录中对所有非隐藏 .ts 文件(名称不以 .
开头)进行处理,忽略隐藏目录和名为 node_modules 的目录。TypeScript 配置可以更改要运行的文件列表。
include
最常见的包含文件方式是在 tsconfig.json 中使用顶级 "include"
属性。它允许一个字符串数组来描述要包含在 TypeScript 编译中的目录和/或文件。
例如,以下配置文件递归地包含相对于 tsconfig.json 的 src/ 目录中的所有 TypeScript 源文件:
{
"include": ["src"]
}
include
字符串中允许使用 glob 通配符来更细粒度地控制要包含的文件:
*
匹配零个或多个字符(不包括目录分隔符)。?
匹配任何一个字符(不包括目录分隔符)。**/
匹配任意嵌套的目录。
以下配置文件只允许嵌套在 typings/ 目录中的 .d.ts 文件和在其扩展名前至少有两个字符的 src/ 文件:
{
"include": [ "typings/**/*.d.ts", "src/**/*??.*"]
}
对于大多数项目,一个简单的 include
编译选项(如 ["src"]
)通常就足够了。
exclude
项目的 include
文件列表有时包括不需要由 TypeScript 编译的文件。TypeScript 允许在 TSConfig 文件通过在顶层指定一个 "exclude"
属性来省略 include
中的路径。与 include
类似,它允许使用一个字符串数组来描述要从 TypeScript 编译中排除的目录和/或文件。
以下配置包括 src/ 中的所有文件,但任何嵌套 external/ 目录和 node_modules 模块目录中的文件除外:
{
"exclude": ["**/external", "node_modules"],
"include": ["src"]
}
默认情况下,exclude
属性包含 ["node modules", "bower components", "jspm packages"]
,以避免在已编译的第三方库文件上运行 TypeScript 编译器。
如果你正在编写自己的排除列表,通常不需要重新添加 “bower components” 或 “jspm packages”。大多数将 node 模块安装到项目中的文件夹的 JavaScript 项目只会安装到 “node_modules”。
请记住,exclude
仅从 include
中的起始列表中删除文件。即使被导入的文件在 exclude
中被明确列出,TypeScript 也会在任何被包含文件导入的文件上运行。
替代扩展名
TypeScript 默认情况下能够读取任何扩展名为 .ts 的文件。但是,某些项目需要能够读取具有不同扩展名的文件,例如 JSON 模块或 UI 库(如 React)的 JSX 语法。
JSX 语法
像 <Component />
这样的 JSX 语法在 UI 库(如 Preact 和 React)中使用。JSX 语法在技术上不是 JavaScript。与 TypeScript 的类型定义一样,它是 JavaScript 语法的扩展,可以编译为常规 JavaScript:
const MyComponent = () => {
// 等同于:
// return React.createElement("div", null, "Hello, world!");
return <div>Hello, world!</div>;
};
为了在文件中使用 JSX 语法,必须执行两项操作:
- 在配置选项中启用
"jsx"
编译选项 - 使用 .tsx 扩展名命名该文件
JSX
用于 "jsx"
编译选项的值决定了 TypeScript 如何为 .tsx 文件生成 JavaScript 代码。项目通常使用这三个值之一(表 13-1)。
*表 13-1 .JSX 编译选项输入和输出
值 | 输入代码 | 输出代码 | 输出文件扩展名 |
---|---|---|---|
“preserve” | <div /> | <div /> | .jsx |
“react” | React.createElement("div") | .js | |
“react-native” | <div /> | <div /> | .js |
jsx
的值可以提供给 tsc
CLI 和/或 TSConfig 文件。
tsc --jsx preserve
{
"compilerOptions": {
"jsx": "preserve"
}
}
如果你没有直接使用 TypeScript 的内置转译器(例如在使用 Babel 等单独的工具进行转译代码时),你很可能可以使用任何允许的值作为 "jsx"
的配置选项。大多数基于现代框架(如 Next.js 或 Remix)构建的 Web 应用程序都会处理 React 的配置和编译语法。如果你正在使用其中一个框架,你可能不需要直接配置 TypeScript 的内置转译器。
.tsx 文件中的泛型箭头函数
第 10 章 “泛型”提到泛型箭头函数的语法与 JSX 语法冲突。在 .tsx 文件中,尝试为箭头函数编写类型参数 <T>
会导致语法错误,因为没有为开放的 T
元素没有提供结束标记:
const identity = <T>(input: T) => input;
// ~~~
// 错误:JSX 元素“T”没有相应的结束标记
为了解决此语法歧义,可以在类型参数中添加 = unknown
约束。类型参数默认为 unknown
类型,因此这根本不会更改代码行为。它只是指示 TypeScript 读取类型参数,而不是 JSX 元素:
const identity = <T = unknown>(input: T) => input; // Ok
解析 Json 模块
如果将 resolveJsonModule
编译选项设置为 true
,TypeScript 将允许读取 .json 文件。如果是这样,可 .json 文件可以像导出对象的 .ts* 文件一样进行导入。TypeScript 将推断该对象的类型,就好像它是一个 const
变量。
对于包含对象的 JSON 文件,可以使用解构导入。下面这对文件在 activist.json 文件中定义了一个 "activist"
字符串,并将其导入到 usesActivist.ts文件中:
// activist.json
{
"activist": "Mary Astell"
}
// usesActivist.ts
import { activist } from "./activist.json";
// Logs: “Mary Astell”
console.log(activist);
如果启用了 esModuleInterop
编译选项(本章稍后将介绍),也可以使用默认导入:
// useActivist.ts
import data from "./activist.json";
对于包含其他字面量类型(如数组或数字)的 JSON 文件,必须使用 as
导入语法。下面这对文件在 activists.json 文件中定义了一个字符串数组,然后将其导入到 useActivists.ts 文件中:
// activists.json
[
"Ida B. Wells", "Sojourner Truth", "Tawakkul Karmān"
]
// useActivists.ts
import * as activists from "./activists.json";
// 打印:"3 activists"
console.log(`${activists.length} activists`);
生成文件
尽管像 Babel 这样的专用编译器工具的兴起 TypeScript 已经减少了 TypeScript 在某些项目中的作用,但许多其他项目仍然依赖 TypeScript 来将 TypeScript 语法编译为 JavaScript。对于项目来说,能够只依赖于 typescript
这一个依赖项,并使用其 tsc
命令输出相应的 JavaScript 是非常有用的。
outDir
默认情况下,TypeScript 将输出文件放置在相应源文件旁边。例如,对包含 fruits/apple.ts 和 vegetables/zucchini.ts 的目录运行 tsc
会生成输出文件 fruits/apple.js 和 vegetables/zucchini.js:
fruits/
apple.js
apple.ts
vegetables/
zucchini.js
zucchini.ts
有时最好将输出文件放在不同的文件夹中。例如,许多 Node 项目将转换后的输出放在 dist 或 lib 目录中。
TypeScript 的 outDir
编译选项允许指定一个不同的根目录用于输出。输出文件的相对目录结构与输入文件保持一致。
例如,在上一个目录上运行 tsc --outDir dist
会将输出文件放置在 dist/ 文件夹中:
dist/
fruits/
apple.js
vegetables/
zucchini.js
fruits/
apple.ts
vegetables/
zucchini.ts
TypeScript 通过查找所有输入文件的最长公共子路径(排除 .d.ts 声明文件)来计算要放置输出文件的根目录。这意味着将所有输入源文件放在单个目录中的项目将把该目录视为根目录。
例如,如果上面的例子将所有输入放在 src/ 目录中并使用 –outDir lib
编译,则会创建 lib/fruits/apple.js 而不是 lib/src/fruits/apple.js:
lib/
fruits/
apple.js
vegetables/
zucchini.js
src/
fruits/
apple.ts
vegetables/
zucchini.ts
确实存在 rootDir
编译选项来显式指定该根目录,但它很少是必需的,也很少与 .
或 src
以外的值一起使用。
target
TypeScript 能够生成输出 JavaScript,可以在 ES3 (大约 1999 年!)的环境中运行。大多数环境都能够支持来自较新版本的 JavaScript 的语法功能。
TypeScript 包含一个 target
编译选项,用于指定需要将 JavaScript 向后转译的语法版本。尽管如果未指定, target
出于向后兼容性原因默认为 "es3"
,而 tsc --init
默认指定为 "es2016"
,但通常建议根据目标平台使用尽可能新的 JavaScript 语法。在旧环境中支持较新的 JavaScript 功能需要创建更多的 JavaScript 代码,这会导致稍大的文件大小和稍差的运行时性能。
截至 2022 年,去年 > 0.1% 的全球用户提供服务的浏览器的所有版本至少支持 ECMAScript 2019 的所有版本和几乎所有的 ECMAScript 2020-2021,而 LTS 支持的 Node.js 版本支持所有 ECMAScript 2021。几乎没有理由不设定至少与 "es2019" 一样高的目标。
例如,以包含 ES2015 const
和 ES2020 ??
判断运算符语法的 TypeScript 源码为例:
无效合并:
function defaultNameAndLog(nameMaybe: string | undefined) {
const name = nameMaybe ?? "anonymous"; console.log("From", nameMaybe, "to", name);
return name;
}
使用 tsc --target es2020
或更新,const
和 ??
是受支持的语法功能,因此 TypeScript 只需要删除 : string | undefined
:
function defaultNameAndLog(nameMaybe) {
const name = nameMaybe ?? "anonymous";
console.log("From", nameMaybe, "to", name);
return name;
}
使用 tsc --target es2015
到 es2019
,??
语法糖将被编译为旧版本的 JavaScript 中的等效项:
function defaultNameAndLog(nameMaybe) {
const name = nameMaybe !== null && nameMaybe !== void 0
? nameMaybe
: "anonymous";
console.log("From", nameMaybe, "to", name);
return name;
}
使用 tsc --target es3
或 es5
,还需要将 const
转换为其等效的 var
:
function defaultNameAndLog(nameMaybe) {
var name = nameMaybe !== null && nameMaybe !== void 0
? nameMaybe
: "anonymous";
console.log("From", nameMaybe, "to", name);
return name;
}
将 target
编译选项设置为与代码运行的最旧环境匹配的值,将确保代码以现代、简洁的语法进行输出,同时仍然可以运行而不会产生语法错误。
生成声明文件
第 11 章 “声明文件”介绍了如何在包中分发 .d.ts 声明文件,以向消费者指示代码类型。大多数软件包使用 TypeScript 的 declaration
编译选项从源文件生成 .d.ts 输出文件:
tsc --declaration
{
"compilerOptions": {
"declaration": true
}
}
.d.ts 输出文件遵循与 .js 文件相同的输出规则,包括遵守 outDir
选项。
例如,在一个包含 fruits/apple.ts和 vegetables/zucchini.ts 的目录上运行 tsc --declaration
将会生成相应的声明文件fruit/apple.d.ts 和 vegetables/zucchini.d.ts,与输出的 .js 文件放置在一起:
fruits/
apple.d.ts
apple.js
apple.ts
vegetables/
zucchini.d.ts
zucchini.js
zucchini.ts
emitDeclarationOnly
还有一个 emitDeclarationOnly
编译选项,它是 declaration
编译选项的专门补充,编译选项的一个专用补充,用于指示TypeScript 仅生成声明文件,而不生成任何 .js/.jsx 文件。这对于使用外部工具生成输出 JavaScript 文件,但仍希望使用 TypeScript 生成输出定义文件的项目非常有用:
tsc --emitDeclarationOnly
{
"compilerOptions": {
"emitDeclarationOnly": true
}
}
如果启用了 emitDeclarationOnly
,则必须启用 declaration
或本章后面介绍的 composite
编译选项。
例如,对包含 fruits/apple.ts和 vegetables/zucchini.ts 的目录运行 tsc --declaration --emitDeclarationOnly
将会输出声明文件 fruits/apple.d.ts 和 vegetables/zucchini.d.ts,不会输出任何 .js 文件:
fruits/
apple.d.ts
apple.ts
vegetables/
zucchini.d.ts
zucchini.ts
源映射(Source Maps)
源映射是描述输出文件与原始源文件之间内容匹配关系的信息。开发工具(如调试器)在导航输出文件时显示原始源代码。它们对于可视化调试器特别有用,例如浏览器开发工具和集成开发环境(IDE),可以在调试过程中查看原始源文件的内容。TypeScript 包括输出源映射文件的功能,与输出文件一起生成。
sourceMap
TypeScript 的 sourceMap
编译选项允许在 .js 或 .jsx 输出文件旁边输出 .js.map 或 .jsx.map 源映射文件。否则,源映射文件通常与对应的 JavaScript 输出文件具有相同的名称,并放置在相同的目录中。
例如,对包含 fruits/apple.ts 和 vegetables/zucchini.ts 的目录运行 tsc --sourceMap
将会在输出的 .js 文件旁边生成输出的源映射文件 fruit/apple.js.map 和 vegetables/zucchini.js.map:
fruits/
apple.js
apple.js.map
apple.ts
vegetables/
zucchini.js
zucchini.js.map
zucchini.ts
declarationMap
TypeScript 还可以为为 .d.ts 声明文件生成源映射。它的 declarationMap
编译选项指示生成一个 .d.ts.map 源映射文件,将其映射回原始源文件。声明映射使得I IDE(如 VS Code)能够在使用编辑器功能(如转到定义)时能够跳转到原始源文件。
declarationMap 在处理项目引用时特别有用,本章末尾将介绍。
例如,对包含 fruits/apple.ts 和 vegetables/zucchini.ts 的目录运行 tsc --declaration --declarationMap
将输出的 .d.ts 和 .js 文件旁边输出声明源映射文件 fruits/apple.d.ts.map 和 vegetables/zucchini.d.ts.map:
fruits/
apple.d.ts
apple.d.ts.map
apple.js
apple.ts
vegetables/
zucchini.d.ts
zucchini.d.ts.map
zucchini.js
zucchini.ts
noEmit
对于完全依赖其他工具将源文件编译为输出 JavaScript 的项目,可以告诉 TypeScript 完全跳过生成文件的步骤。启用 noEmit
编译选项将使 TypeScript 仅充当类型检查器。
在之前的任何示例中运行 tsc --noEmit
将导致不会创建新文件。TypeScript 只会报告它发现的任何语法或类型错误。
类型检查
大多数 TypeScript 的配置选项都控制着它的类型检查器。你可以把它配置为温和宽容,只在完全确定错误时才发出类型检查的警告,也可以配置为严厉苛刻,要求几乎所有代码都要有良好的类型。
lib
首先,TypeScript 假设在运行时环境中存在的全局 API 是可以使用 lib
编译选项进行配置的。它接受一个字符串数组,默认为 target
编译选项,以及 dom
以指示包括浏览器类型。
大多数情况下,自定义 lib
的唯一原因是为了在不运行在浏览器中的项目的移除 dom
的包含:
tsc --lib es2020
{
"compilerOptions": {
"lib": ["es2020"]
}
}
或者,对于使用 polyfill 来支持较新的 JavaScript API 的项目,lib
可以包含 dom
和任何 ECMAScript 版本:
tsc --lib dom,es2021
{
"compilerOptions": {
"lib": ["dom", "es2021"]
}
}
在修改 lib
时 要小心,要确保提供所有正确的运行时 polyfill。如果一个项目的 lib
设置为 "es2021"
,但在运行时环境中只支持 ES2020 或更早的版本,那么该项目可能不会出现类型检查错误,但在尝试使用 ES2021 或更高版本中定义的 API(例如 String.replaceAll
)时可能会遇到运行时错误:
const value = "a b c";
value.replaceAll(" ", ", ");
// 未捕获的TypeError:value.replaceAll不是一个函数
可以将lib
编译选项视为指示可用的内置语言 API,而target
目标编译选项则指示存在哪些语法特性。
skipLibCheck
TypeScript 提供了一个 skipLibCheck
的编译选项,用于指示跳过对在源代码中未明确包含的声明文件进行的类型检查。这对于许多可能依赖于共享库的不同且可能存在冲突定义的应用程序非常有用。
tsc --skipLibCheck
{
"compilerOptions": {
"skipLibCheck": true
}
}
skipLibCheck
通过允许 TypeScript 跳过某些类型检查来提高 TypeScript 的性能。因此,通常在大多数项目中启用它是一个好主意。
严格模式
大多数 TypeScript 的类型检查编译选项都分组到 TypeScript 所称的 严格模式。每个严格模式编译选项的默认值为 false
,启当启用时,会指示类型检查器打开一些其他检查。
在本章后面,我将按字母顺序介绍最常用的严格模式选项。在这些选项中,noImplicitAny
和 strictNullChecks
在强制执行类型安全的代码方面特别有用和有影响力。
你可以通过启用 strict
编译选项来启用所有严格模式检查:
tsc --strict
{
"compilerOptions": {
"strict": true
}
}
如果要启用除某些检查之外的所有严格模式检查,则可以启用 strict
并显式禁用某些检查。例如,下面配置启用除 noImplicitAny
之外的所有严格模式:
tsc --strict --noImplicitAny false
{
"compilerOptions": {
"noImplicitAny": false,
"strict": true
}
}
TypeScript 的未来版本可能会在 strict 下引入新的严格类型检查编译选项。因此,在更新 TypeScript 版本时,使用 strict 可能会导致新的类型检查投诉。你可以随时选择退出 TSConfig 中的特定设置。
noImplicitAny
如果 TypeScript 无法推断参数或属性的类型,则它将回落到假设的 any
类型。通常最好的做法是不允许在代码中使用隐式 any
类型,因为允许 any
类型绕过大部分 TypeScript 的类型检查。
noImplicitAny
编译选项指示 TypeScript 在必须回落到隐式 any
时发出类型检查警告。
例如,如果在没有类型声明的情况下编写以下函数参数,将会在 noImplicitAny
下引发类型错误:
const logMessage = (message) => {
// ~~~~~~~
// 错误:参数“message”隐式具有“any”类型。
console.log(`Message: ${message}!`);
};
大多数情况下,可以通过在出现警告的位置添加类型注解来解决 noImplicitAny
的警告:
const logMessage = (message: string) => { // Ok
console.log(`Message: ${message}!`);
}
或者,对于函数参数,可以通过将父函数放在指示函数类型的位置来解决警告:
type LogsMessage = (message: string) => void;
const logMessage: LogsMessage = (message) => { // Ok
console.log(`Message: ${message}!`);
}
noImplicitAny 是确保整个项目类型安全的的优秀标志。我强烈建议在完全用 TypeScript 编写的项目中尝试打开它。但是,如果项目仍在从 JavaScript 转换为 TypeScript的过程中,最好先完成将所有文件转换为 TypeScript 的工作。
strictBindCallApply
当 TypeScript 首次发布时,它没有足够丰富的类型系统功能来表示内置的 Function.apply
、Function.bind
或 Function.call
函数实用程序。默认情况下,这些函数必须为其参数列表输入 any
。这不是很安全!
例如,如果没有 strictBindCallApply
,get Length
的以下变体都在其类型中包含 numder
(原文 any
):
function getLength(text: string, trim?: boolean) {
return trim ? text.trim().length : text.length;
}
// 函数类型:(thisArg: Function, argArray?: any) => number
getLength.apply;
// 返回类型:number
getLength.bind(undefined, "abc123");
// 函数类型:number
getLength.call(undefined, "abc123", true);
现在 TypeScript 的类型系统功能足够强大,可以表示这些函数的泛型剩余参数,TypeScript 允许选择对函数使用更严格的类型。
启用 strictBindCallApply
可以为 getLength
变体启用更精确的类型:
function getLength(text: string, trim?: boolean) {
return trim ? text.trim().length : text;
}
// Function type:
// (thisArg: typeof getLength, args: [text: string, trim?: boolean]) => number;
getLength.apply;
// Returned type: (trim?: boolean) => number
getLength.bind(undefined, "abc123");
// Returned type: number
getLength.call(undefined, "abc123", true);
TypeScript 最佳做法是启用 strictBindCallApply
。它改进了对内置函数实用程序的类型检查,有助于提高使用它们的项目的类型安全性。
strictFunctionTypes
strictFunctionTypes
编译选项会使函数参数类型的检查更加严格。如果一个函数类型的参数是另一个函数类型参数的子类型,那么这个函数类型将不再被认为可以赋值给另一个函数类型。
具体来说,在这个例子中中,checkOnNumber
函数接收一个函数作为参数,该函数应该能够接收 number |string
类型的参数,但是实际上却提供了一个stringContainsA
函数,该函数只能接收string类型的参数。TypeScript的默认类型检查会允许这种情况发生,但是程序会由于尝试在 number
上调用 .match()
方法而崩溃:
function checkOnNumber(containsA: (input: number | string) => boolean) {
return containsA(1337);
}
function stringContainsA(input: string) {
return !!input.match(/a/i);
}
checkOnNumber(stringContainsA);
在 strictFunctionTypes
下,checkOnNumber(stringContainsA)
将导致类型检查错误:
// 类型“(input: string) => boolean”的参数不能赋给类型“(input: string | number) => boolean”的参数。
// 参数“input”和“input” 的类型不兼容。
// 不能将类型“string | number”分配给类型“string”。
// 不能将类型“number”分配给类型“string”。
checkOnNumber(stringContainsA);
在技术术语中,函数参数从 双变 变为 逆变。你可以在TypeScript 2.6 发行说明.
strictNullChecks
回到第 3 章“联合和字面量”中,我讨论了语言的数十亿美元的错误:允许空类型(如null
和undefined
)赋值给非空类型。禁用 TypeScript 的 strictNullChecks
标志在代码中的每个类型后面添加 null |undefined
,从而允许任何变量接收 null
或 undefined
。
当启用strictNullChecks
时,以下代码片段将导致将 null
赋值给 string
类型的变量时抛出类型错误:
只有当启用了 strictNullChecks
时,以下代码片段才会导致将 null
分配给 string
类型的变量时抛出类型错误:
let value: string;
value = "abc123"; // 总是正确
value = null;
// 启用 strictNullChecks:
// 错误:不能将类型“null”分配给类型“string”
TypeScript 最佳实践是启用 strictNullChecks
。这样做有助于防止程序崩溃并消除了这个价值数十亿美元的错误。
详细信息请参考第 3 章“联合类型和字面量”。
strictPropertyInitialization
在第8章“类”中,我讨论了类中的严格初始化检查:确保类的每个属性在类构造函数中被明确赋值。TypeScript 的 strictPropertyInitialization
标志会在类属性没有初始化器并且在构造函数中没有明确赋值时抛出类型错误。
通常,TypeScript 的最佳实践是启用 strictPropertyInitialization
。这样做有助于防止因类初始化逻辑中的错误而导致崩溃。
详细信息请参考第 8 章“类”。
useUnknownInCatchVariables
任何语言的错误处理本质上都是一个不安全的概念。理论上,任何函数都可以抛出任意数量的错误,例如读取 undefined
的属性或用户编写的 throw
语句等边缘情况。事实上,无法保证抛出的错误甚至是 Error
类的实例:代码始终可以 throw "something else"
。
因此,TypeScript 对于错误的默认行为是将它们的类型设置为 any
,因为它们可以是任何类型。这在错误处理方面提供了灵活性,但默认情况下依赖于不太类型安全的 any
。
下面的代码片段中的 error
的类型是 any
,因为 TypeScript 无法知道 someExternalFunction()
可能抛出的所有可能错误:
try {
someExternalFunction();
} catch (error) {
error; // 默认类型:any
}
和大多数 any
的使用情况一样,更为严谨的做法是将错误的类型视为 unknown
,尽管这可能需要进行显式的类型断言或缩小类型的操作。在catch
子句中,错误可以被注释为 any
或 unknown
类型。
下面是对代码片段进行修正,将 error
的类型显式地修改为 unknown
:
let error: unknown;
try {
someExternalFunction();
} catch (e) {
error = e;
}
在 TypeScript 中,启用 useUnknownInCatchVariables
严格标志可以将 catch 子句中的错误类型默认设置为 unknown
。如果启用了 useUnknownInCatchVariables
,那么两个代码片段中的 error 变量的类型都将被设置为 unknown。
一般来说,TypeScript 最佳实践是启用 useUnknownInCatchVariables
,因为不能总是安全地假设错误将是特定的类型。通过将错误类型设置为 unknown,我们能够更好地处理错误,并进行必要的类型检查。
try {
someExternalFunction();
} catch (error: unknown) {
error; // 类型:unknown
}
严格区域标志 useUnknownInCatchVariables
将 TypeScript 的默认 catch 子句错误类型更改为 unknown
。启用 useUnknownInCatchVariables
后,两个代码段的 error
类型都将设置为 unknown
。
TypeScript 最佳实践通常是启用 useUnknownInCatchVariables
,因为假设错误是任何特定类型并不总是安全的。
模块
JavaScript 中用于导出和导入模块内容的各种系统(如AMD、CommonJS、ECMAScript等)是现代编程语言中最复杂的模块系统之一。相对而言,JavaScript 的文件之间相互导入的方式通常由用户编写的框架(如 Webpack)驱动。
TypeScript 尽最大努力提供配置选项,以表示大多数合理的用户模块配置。
大多数新的 TypeScript项目都使用标准化的 ECMAScript 模块语法。以下是 ECMAScript 模块如何从另一个模块( "my-example-lib"
)导入一个值(value
)并导出自己的值(logValue
)的概述:
import { value } from "my-example-lib";
export const logValue = () => console.log(value);
module
TypeScript 提供了一个 module
的编译选项,用于指定转译后的代码将使用哪种模块系统。当使用 ECMAScript 模块编写源代码时,TypeScript 可能会根据 module
值将 export
和 import
语句转译到不同的模块系统。
例如,通过以下命令行指定 ECMAScript 项目将以 CommonJS 模块形式输出:
tsc --module commonjs
或在 TSConfig 中:
{
"compilerOptions": {
"module": "commonjs"
}
}
上述代码片段的大致输出结果如下:
const myexamplelib = require("my-example-lib");
exports.logValue = () => console.log(myexamplelib.value);
如果 target
编译选项是 "es3"
或 "es5"
,module
的默认值将是 "commonjs"
。否则,module
默认值将为 "es2015"
表示输出 ECMAScript 模块。
moduleResolution
模块解析(Module resolution)是将导入语句中的路径映射到模块的过程。TypeScript提供了 moduleResolution
选项,你可以使用它来指定解析过程的逻辑。通常,你可以选择以下两种逻辑策略之一:
- node:与传统Node.js等CommonJS解析器使用的行为相符
- nodenext:与ECMAScript模块指定的行为对齐
这两种策略类似。大多数项目可以使用其中任何一种而不会注意到差异。你可以在https://www.typescriptlang.org/docs/handbook/module-resolution.html 上了解有关模块解析背后的详细信息。
moduleResolution
不会改变T ypeScript 的代码生成方式。它仅用于描述代码应该在其中运行的运行时环境。
以下 CLI 代码段和 JSON 文件代码段都可以指定 moduleResolution
编译选项:
tsc --moduleResolution nodenext
{
"compilerOptions": {
"moduleResolution": "nodenext"
}
}
出于向后兼容的原因,TypeScript 将默认的 moduleResolution 值保留为多年前用于项目的经典值(classic
)。在任何现代项目中,你几乎肯定不希望使用经典策略。
与 CommonJS 的互操作性
在使用 JavaScript 模块时,默认导出和命名空间导出之间有一个区别。模块的默认导出是其导出对象上的 .default
属性。模块的命名空间导出是导出的对象本身。
表 13-2 概述了默认和命名空间导出和导入之间的差异
表 13-2.CommonJS 和 ECMAScript 模块导出和导入表单
语法区域 | 通用 JS | ECMAScript 模块 |
---|---|---|
默认导出 | module.exports.default = value; | export default value; |
默认导入 | const { default: value } = require("..."); | import value from "..."; |
命名空间导出 | module.exports = value; | 不支持 |
命名导入 | const value = require("..."); | import * as value from "..."; |
TypeScript 的类型系统以 ECMAScript 模块的形式构建了对文件导入和导出的理解。然而,如果你的项目依赖于npm包(大多数项目都依赖于npm包),很可能其中一些依赖是以 CommonJS 模块的形式发布的。此外,尽管一些符合 ECMAScript 模块规范的包避免包含默认导出,但许多开发人员更喜欢使用更简洁的默认导入方式,而不是命名空间导入方式。TypeScript 包含了一些编译选项,用于改善不同模块格式之间的互操作性。
esModuleInterop
esModuleInterop
配置选项在 TypeScript 生成的 JavaScript 代码中添加了一小段逻辑,用于处理 module
不是 ECMAScript 模块格式(如 "es2015"
或 "esnext"
)时的情况。该逻辑允许 ECMAScript 模块从其他模块进行导入,即使这些模块不符合 ECMAScript 模块的默认导入或命名空间导入规则。
启用 esModuleInterop
的一个常见原因是一些包(例如 "react"
)不提供默认导出。如果一个模块尝试使用 "react"
包的默认导入方式,在未启用 esModuleInterop
的情况下,TypeScript 会报告类型错误:
import React from "react";
// ~~~~
// 模块 '"file:///nodemodules/@types/react/index"' 只能在使用
// 只能在使用 "esModuleInterop" 标志时进行默认导入
请注意,esModuleInterop
仅直接更改生成的 JavaScript 代码与导入的工作方式。以下 allowSyntheticDefaultImports
配置选项通知类型系统有关导入互操作性的信息。
allowSyntheticDefaultImports
allowSyntheticDefaultImports
编译选项通知类型系统,ECMAScript 模块可能默认从不兼容的 CommonJS 命名空间导出的文件导入。
仅当满足以下任一条件时,它才默认为 true
:
module
是"system"
(本书未涉及的一种较旧的、很少使用的模块格式)。esModuleInterop
是true
,module
不是 ECMAScript 模块格式,例如"es2015"
或"esnext"
。
换句话说,如果 esModuleInterop
为 true
,但 module
设置为 "esnext"
,TypeScript 将假设输出的编译后 JavaScript 代码未使用导入互操作性辅助函数。它会对来自诸如 "react"
的包的默认导入报告类型错误:
import React from "react";
// 模块 '"file:///nodemodules/@types/react/index"' 只能在使用
// 只能在使用 "esModuleInterop" 标志时进行默认导入
isolatedModules
外部的转译器(如 Babel)一次只处理一个文件,无法使用类型系统信息来生成 JavaScript。因此,在这些转译器中,通常不支持依赖类型信息来生成 JavaScript 的 TypeScript 语法特性。启用 isolatedModules
编译选项会告诉 TypeScript 在可能引起这些转译器问题的语法实例上报告错误:
- 常量枚举,在第 14 章“语法扩展”中介绍
- 脚本(非模块)文件
- 独立的类型导出,在第 14 章“语法扩展”中介绍
如果你的项目使用 TypeScript 以外的工具进行转译为 JavaScript,我通常建议启用 isolatedModules
选项。
JavaScript
虽然 TypeScript 很好,我希望你能始终使用它来编写代码,但你并不需要将所有的源文件都用 TypeScript 编写。尽管 TypeScript 默认忽略具有 .js 或 .jsx 扩展名的文件,但使用 allowJs
和/或 checkJs
编译选项可以使其读取、编译甚至在有限的程度上对 JavaScript 文件进行类型检查。
将现有的 JavaScript 项目转换为 TypeScript 的常见策略是最初只将少数文件转换为 TypeScript。随着时间的推移,可以逐步添加更多的文件,直到没有剩余的 JavaScript 文件为止。在你准备好之前,不必完全转向 TypeScript!
允许 Js
allowJs
编译选项允许在类型检查 TypeScript 文件时考虑 JavaScript 文件中声明的构造。当与 jsx
编译选项结合使用时,也允许使用 .jsx 文件。
例如,考虑以下示例,index.ts 导入了在 values.js 文件中声明的 value
:
// index.ts
import { value } from "./values";
console.log(`Quote: '${value.toUpperCase()}'`);
// values.js
export const value = "We cannot succeed when half of us are held back.";
如果未启用 allowJs
,import
语句将没有已知类型。默认情况下,它将隐式为 any
,或触发类型错误,例如无法找到模块“./values”的声明文件。
allowJs
选项还将 JavaScript 文件添加到编译为 ECMAScript 目标并作为 JavaScript 输出的文件列表中。如果启用了相应的选项,还会生成源映射和声明文件。
tsc --allowJs
{
"compilerOptions": {
"allowJs": true
}
}
启用 allowJs
后,导入的 value
将为 string
类型。不会报告任何类型错误。
checkJs
TypeScript 不仅可以将 JavaScript 文件纳入 TypeScript 文件的类型检查,还可以对 JavaScript 文件进行类型检查。checkJs
编译选项有两个目的:
- 如果之前没有设置,将
allowJs
默认设置为true
- 在 .js 和 .jsx 文件上启用类型检查器
tsc --checkJs
{
"compilerOptions": {
"checkJs": true
}
}
启用了 checkJs
选项后,这个 JavaScript 文件中的错误变量名将引发类型检查错误的警告:
// index.js
let myQuote = "Each person must live their life as a model for others.";
console.log(quote);
// ~~~~~~
// 错误 : 找不到名称“quote”。你是否指的是“myQuote”?
如果没有启用 checkJs
选项,TypeScript 将不会报告该潜在错误的类型错误。
@ts-check
或者,可以通过在文件顶部包含 // @ts-check
注释来逐个文件启用 checkJs
。这样做只会为该 JavaScript 文件启用 checkJs
选项:
// index.js
// @ts-check
let myQuote = "Each person must live their life as a model for others.";
console.log(quote);
// ~~~~~~
// 错误 : 找不到名称“quote”。你是否指的是“myQuote”?
JSDoc 支持
因为 JavaScript 并没有像 TypeScript 一样丰富的类型语法,所以在 JavaScript 文件中声明的值的类型通常不像在 TypeScript 文件中声明的那样精确。例如,尽管 TypeScript 可以推断出在 JavaScript 文件中声明为变量的对象的值,但在该文件中没有原生的 JavaScript 方式来声明该值符合任何特定的接口。
我在第1章“从 JavaScript 到 TypeScript”中提到过,JSDoc 社区标准提供了一些使用注释描述类型的方式。当启用了 allowJs
和/或 checkJs
选项时,TypeScript 将识别代码中的任何 JSDoc 定义。
例如,这个代码片段在 JSDoc 中声明了 sentenceCase
函数接受一个 string
类型的参数。TypeScript 可以推断出它返回一个 string
类型。当启用了 checkJs
选项后,TypeScript 会报告将 string[]
类型的值传递给它的类型错误。
// index.js
/**
* @param {string} text
*/
function sentenceCase(text) {
return `${text[0].toUpperCase()} ${text.slice(1)}.`;
}
sentenceCase("hello world");// Ok
sentenceCase(["hello", "world"]);
// ~~~~~~~~~~~~~~~~~~
// 错误:类型“string[]”的参数不能赋给类型“string”的参数。
TypeScript 的 JSDoc 支持对于为没有时间或开发人员熟悉转换为 TypeScript 的项目增量添加类型检查非常有用。
支持的 JSDoc 语法的完整列表可访问 https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html。
配置扩展
随着你编写越来越多的 TypeScript 项目,你可能会发现自己反复编写相同的项目设置。虽然 TypeScript 不允许在配置文件中使用 JavaScript 的 import
或 require
,但它提供了一种机制,可以从另一个配置文件中选择“继承”或复制配置值的方式。这个机制通过 TSConfig 文件来实现。
扩展
TSConfig 可以使用 extends
配置选项从另一个TSConfig继承。extends
接受另一个 TSConfig 文件的路径,并指示应该复制该文件中的所有设置。它的行为类似于类上的 extends
关键字:在派生的(子)配置上声明的任何选项将覆盖基础的(父)配置上具有相同名称的选项。
例如,许多具有多个 TSConfig 的代码库(例如包含多个 packages/ 目录的 monorepo)通常会创建一个 tsconfig.base.json 文件供 tsconfig.json 文件继承:
// tsconfig.base.json
{
"compilerOptions": {
"strict": true
}
}
// packages/core/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"includes": ["src"]
}
请注意,compilerOptions
会递归地进行合并。基础 TSConfig 的每个编译选项都会复制到派生 TSConfig,除非派生 TSConfig 覆盖了该特定选项。
如果前面的示例添加了一个包含 allowJs
选项的 TSConfig,那个新的派生 TSConfig 仍然会将 compilerOptions.strict
设置为true
:
// packages/js/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true
},
"includes": ["src"]
}
扩展模块
extends
属性可以指向两种类型的 JavaScript 导入:
- 绝对路径
以@
或字母开头 - 相对路径
以.
开头的本地文件路径
当 extends
的值是绝对路径时,它表示从一个 npm 模块继承 TSConfig。TypeScript 将使用正常的 Node 模块解析系统来找到与名称匹配的包。如果该包的 package.json
包含一个包含相对路径字符串的 "tsconfig"
字段,则将使用该路径下的 TSConfig 文件。否则,将使用该包的 tsconfig.json 文件。
许多组织使用 npm 包来在存储库之间或在单体存储库内标准化 TypeScript 编译选项。以下是你在 @my-org
组织的单体存储库中可能设置的 TSConfig 文件示例。packages/js
需要指定 allowJs
编译选项,而 packages/ts
不会更改任何编译选项:
// packages/tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
// packages/js/tsconfig.json
{
"extends": "@my-org/tsconfig",
"compilerOptions": {
"allowJs": true
},
"includes": ["src"]
}
// packages/ts/tsconfig.json
{
"extends": "@my-org/tsconfig",
"includes": ["src"]
}
配置基础
与从头开始创建自己的配置文件或使用 init
建议相比,你可以从一个预先制定的“基础”TSConfig文件开始,该文件针对特定的运行时环境进行了定制。这些预定义的配置基础可以在 npm 包注册表中找到,位于 @tsconfig/
下,例如 @tsconfig/recommended
或 @tsconfig/node16
。
例如,要安装适用于 deno
的推荐 TSConfig 基础配置:
npm install --save-dev @tsconfig/deno
*# 或者*
yarn add --dev @tsconfig/deno
安装完配置基础包后,可以像其他 npm 包的配置扩展一样引用它:
{
"extends": "@tsconfig/deno/tsconfig.json"
}
完整的 TSConfig 基础列表可以在 https://github.com/tsconfig/bases 上找到。
通常,了解你的文件使用的 TypeScript 配置选项是一个好主意,即使你自己不打算更改它们。
项目引用
到目前为止,我展示的每个 TypeScript 配置文件都假设它们管理了整个项目的所有源文件。在较大的项目中,使用不同的配置文件管理项目的不同部分可能会很有用。TypeScript 允许定义一个“项目引用”系统,可以同时构建多个项目。设置项目引用需要比使用单个 TSConfig 文件更多的工作量,但带来了几个关键的好处:
- 你可以为特定代码区域指定不同的编译选项。
- TypeScript 将能够为单个项目缓存构建输出,这通常可以显著加快大型项目的构建时间。
- 项目引用强制执行“依赖树”,只允许特定的项目从特定的其他项目导入文件,这有助于结构化代码的离散区域。
项目引用通常用于具有多个不同代码区域的大型项目,例如 monorepo 和模块化组件系统。对于没有几十个或更多文件的小型项目,你可能不想使用它们。
以下的三个部分展示了如何建立项目设置以启用项目引用:
- 在 TSConfig 上启用
composite
模式,以适合多 TSConfig 构建模式的方式工作。 - 在 TSConfig 中使用
references
指定它所依赖的复合 TSConfig。 - 构建模式使用复合 TSConfig 引用来协调构建它们的文件。
composite
TypeScript 允许项目选择使用 composite
配置选项,以指示其文件系统的输入和输出遵循约束条件,使构建工具更容易确定其构建输出是否与构建输入保持最新。当 composite
设置为 true
时:
- 如果未明确设置,
rootDir
设置将默认为包含 TSConfig 文件的目录。 - 所有实现文件必须与 include 模式匹配或列在
files
数组中。 - 必须打开
declaration
。
以下配置片段符合在 core/
目录中启用 composite
模式的所有条件:
// core/tsconfig.json
{
"compilerOptions": {
"declaration": true
},
"composite": true
}
这些更改可帮助 TypeScript 强制项目的所有输入文件创建匹配的 .d.ts 文件。composite
通常与以下 references
配置选项结合使用最有用。
references
TypeScript 项目可以通过在其 TSConfig 中设置 references
来指示其依赖于由一个复合 TypeScript 项目生成的输出。从引用项目导入模块将在类型系统中被视为从其输出的 .d.ts 声明文件中导入。
以下配置片段设置了一个 shell/ 目录,以引用 core/ 目录作为其输入:
// shell/tsconfig.json
{
"references": [
{ "path": "../core" }
]
}
references
配置选项不会通过 extends
从基础 TSConfig 复制到派生 TSConfig。
references
配置选项通常与以下构建模式结合使用,具有最大的效用。
构建模式
一旦代码区域已经设置为使用项目引用,就可以通过 -b/--b
命令行标志将 tsc
切换到其替代的“构建”模式。构建模式将 tsc
扩展为一种项目构建协调器。它使得 tsc
仅重新构建自上次构建以来发生更改的项目,基于其内容和文件输出上次生成的时间。
具体而言,TypeScript 的构建模式在给定一个 TSConfig 时会执行以下操作:
- 查找该 TSConfig 引用的项目。
- 检测它们是否是最新的。
- 按正确的顺序构建需要更新的项目。
- 如果提供的 TSConfig 或其任何依赖项已更改,则构建该 TSConfig。
TypeScript 构建模式能够跳过重新构建最新的项目,从而显著提高构建性能。
协调器配置
在一个代码库中,设置 TypeScript 项目引用的常见模式是在根级别的 tsconfig.json
中设置一个空的 files
数组,并引用该代码库中所有的项目引用。根级别的 TSConfig 不会直接指示 TypeScript 构建任何文件,它只会告诉 TypeScript 在需要时构建引用的项目。
下面的 tsconfig.json
表示构建代码库中的 packages/core
和 packages/shell
项目:
// tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/shell" }
]
}
我个人喜欢在 package.json
中标准化一个名为 build
或 compile
的脚本,用于快捷调用 tsc -b
命令:
// package.json
{
"scripts": {
"build": "tsc -b"
}
}
构建模式选项
构建模式支持一些特定于构建的 CLI 选项:
--clean
:删除指定项目的输出(可以与--dry
结合使用)--dry
:显示将要执行的操作,但不实际构建任何内容--force
:将所有项目视为过时,强制重新构建-w
/--watch
:类似于典型的 TypeScript 监视模式
由于构建模式支持监视模式,可以运行类似 tsc -b -w
的命令,以快速获得大型项目中所有编译器错误的最新列表。
总结
在本章中,你学习了许多 TypeScript 提供的重要配置选项:
- 使用
tsc
命令,包括其美化和监视模式 - 使用 TSConfig 文件,包括使用
tsc --init
创建一个TSConfig文件 - 更改 TypeScript 编译器要包含的文件
- 允许在 .tsx 文件中使用JSX语法和/或在 .json 文件中使用 JSON 语法
- 使用
files
选项更改输出的目录、ECMAScript 版本目标、声明文件和/或源映射文件 - 更改编译时使用的内置库类型
- 严格模式和有用的严格标志,如
noImplicitAny
和strictNullChecks
- 支持不同的模块系统,并更改模块解析方式
- 允许包含 JavaScript 文件,并选择对这些文件进行类型检查
- 使用
extends
在文件之间共享配置选项 - 使用项目引用和构建模式来协调多个 TSConfig 文件的构建
现在你已经读完了这一章,你最好练习一下你学到的东西 https://learningtypescript.com/configuration-options。
守纪人员最喜欢的 TypeScript 编译选项是什么?
严格。
评论 (0)