前言
用过 typescript 的人,肯定都知道 tsconfig.json 文件。tsc 在编译时,会读取这个 json 文件配置的规则,这些规则决定了编译器将会怎么工作。
为什么 tsc 在工作时会提示这个错误,它是由哪个变量控制的;为什么我只是升级 typescript 的版本,会多这么多错误...
很多时候我对这些问题都是一知半解,所以决定把 tsconfig.json 支持的配置项全部过一遍。
苦于官方只有英文文档,打算整理一份中文的,以备后续时常翻阅。绝大部分内容都是直接翻译,有时候我会加些自己的理解,如有不准确或有误的地方,欢迎大家提出。
tsconfig.json 概述
目录中存在 tsconfig.json ,表明该目录是 TypeScript 项目的根目录。tsconfig.json 文件指定编译项目所需的根目录文件和编译选项。
tsconfig.json 最顶层的配置包括 files 、 extends 、 exclude 、 和 references 、 compilerOptions ,绝大部分编译规则都是在 compilerOptions 中设置的,我们主要看这个配置,其他配置后续简单过一下。
compilerOptions 中的配置规则又分很多类别,比如 Type Checking 、Modules 、 Emit 等,我们就按这个分类一个个的过。
Type Checking 类型检测
allowUnreachableCode(released 1.8)
allow unreachable code ,是否允许无法访问的代码存在。该属性值有 3 个:
undefined:(默认) 编辑器显示告警提示;
true:无法访问的代码不做校验
false:无法访问的代码会出现编译错误
警告仅涉及根据 JavaScript 语法而被证明无法访问的代码,例如:
function fn(n: number) {
if (n > 5) {
return true;
} else {
return false;
}
// 这一行代码就是 unreachable code
return true;
}
因为永远不会执行最后一行 return true ,这就属于 unreachable code 。
这种写法不会导致基于代码的错误,但从类型检测分析,永远无法被访问到。
allowUnusedLabels(released 1.8)
allow unused labels ,是否允许未使用的标签存在。该属性值有 3 个:
undefined:(默认) 编辑器显示告警提示;
true:未使用的标签不做校验
false:未使用的标签会引起编译错误
标签语句在 JavaScript 中非常少见,通常情况下,可以使用函数调用而不是(基于标记的)循环跳转。
在给出具体的示例之前,我们先了解一下什么是标签语句。
标签语句是 JavaScript 中一种特殊的语法结构,用于在代码中标记某个语句块,以便在后续的语法中进行跳转。
标签语句可与 break 、continue 等语句配合使用。语法如下:
label: statement
标签语句的代码示例,其中 loop1 就是定义的标签:
let str = '';
loop1: for (let i = 0; i < 5; i++) {
if (i === 1) {
continue loop1;
}
str = str + i;
}
console.log(str); // 0234
如果只是定义了标签,并未使用,并且 allowUnusedLabels 为false,就会出现编译错误,如下图所示:
let str = '';
loop1: for (let i = 0; i < 5; i++) {
str = str + i;
}
console.log(str);
alwaysStrict(released 2.1)
确保你的文件在 ECMAScript 「严格模式」下进行解析,并且对每个源文件发出 "use strict" 指令。
默认值跟 strict 属性相关,如果 strict 为 true ,则该属性也为 true ,反之为 false 。
ECMAScript 严格模式是在 ES5 中引入的,它对 JavaScript 引擎运行时的行为进行调整,以提供性能,而不是忽略它们。
在严格模式下,JavaScript引擎会更严格地执行代码,这可以帮助开发者更早地发现和修复错误。例如,严格模式会禁止使用未声明的变量,这可以防止开发者不小心创建全局变量。此外,严格模式还会对某些可能导致性能下降的操作进行限制。
此外,严格模式还改变了错误处理的方式。在非严格模式下,JavaScript引擎可能会默默地忽略某些错误,这可能会使问题更难以调试。但在严格模式下,这些错误会被抛出,使开发者能够更容易地发现和解决问题。
exactOptionalPropertyTypes(released 4.4)
当启用 exactOptionalPropertyTypes 后,Typescript 会对带有 ? 前缀的类型或接口的属性应用更严格的规则。
例如,这个接口声明了一个属性,它可以是这两个值中的一个: 'dark' 或 'light' 或 它不应该在对象中。
interface UserDefaults {
// The absence of a value represents 'system'
colorThemeOverride?: "dark" | "light";
}
如果没有启用 exactOptionalPropertyTypes ,你可以将 colorThemeOverride 设置成三个值之一: 'dark' 或 'light' 或 'undefined' 。
总结来说:
如果没有启用这个标志,那么一个可选属性可以被赋值为 undefined,这意味着这个属性在对象中存在,但值为undefined。
但如果启用了这个标志,那么一个可选属性不能被赋值为 undefined,如果一个可选属性的值为 undefined,那么这个属性就不能在对象中出现。
假如启用后,硬要将 colorThemeOverride 值设为 undefined,则会提示以下错误:
这使得 TypeScript 能够更准确地描述数据模型,并避免一些潜在的错误。
noFallthroughCasesInSwitch(released 1.8)
报告 swtich 语句中 case 穿透的错误。确保 switch 语句内任何非空的 case ,都包含 break 、return 、 或 throw 。可以及时发现 case 穿透的错误,如下代码就是一个 case 穿透的错误:
const a: number = 6;
switch (a) {
case 0:
// Fallthrough case in switch.
console.log("even");
case 1:
console.log("odd");
break;
}
"fallthrough"是指在switch语句中,一个case没有结束,就自动执行下一个case的情况。
noImplicitAny
不存在隐含的 any 类型。
默认值跟 strict 属性相关,如果 strict 为 true ,则该属性也为 true ,反之为 false 。
在某些情况下,如果没有类型注解,typescript 无法推断出变量的类型,它会默认将变量的类型设为 any 。
这将导致一些错误被忽视,例如:
function fn(s) {
// No error?
console.log(s.subtr(3));
}
fn(42);
如果开启 noImplicitAny ,且没有类型注解时,一旦变量被推断为 any 类型,typescript 将会抛出错误:
function fn(s) {
// error: Parameter 's' implicitly has an 'any' type.
console.log(s.subtr(3));
}
此时可以明确的指定 s 为 any 类型(不建议这么操作),最好是明确定义 s 的类型。
noImplicitOverride(released 4.3)
当使用 class 实现继承时,可能会遇到一个问题,当你有一个子类重载(覆盖)了基类中的某个函数,那么子类可能会“失去同步”,也就是说子类中的重载函数可能不再正确的覆盖基类中的函数,因为基类中的函数已经被重命名了。
例如,假设你正在对音乐专辑同步系统进行建模:
class Album {
download() {
// Default behavior
}
}
class SharedAlbum extends Album {
download() {
// Override to get info from many sources
}
}
然后,当你添加「机器学习生成播放列表的功能」时,你重构了 Album 类,使其有一个 ‘setup’ 函数:
class Album {
setup() {
// Default behavior
}
}
class MLAlbum extends Album {
setup() {
// Override to get info from algorithm
}
}
class SharedAlbum extends Album {
download() {
// Override to get info from many sources
}
}
在这种情况下,如果 SharedAlbum 类中的 download 函数期望重载基类中的一个函数,其实是有问题的,因为基类中此时并没有 download 函数,但是 typescript 却并没有提供警告⚠️。
使用 noImplicitOverride 选项,只要重载的函数包含关键字 override , 你可以确保子类永远不会失去同步。
下面的例子启用了 noImplicitOverride ,当缺少 override 关键字时,你会收到一个错误:
class Album {
setup() {}
}
class MLAlbum extends Album {
override setup() {}
}
class SharedAlbum extends Album {
setup() {}
// 缺少关键词 override
// error: This member must have an 'override' modifier because it overrides a member in the base class 'Album'.
}
总的来说,这个选项开启后,当通过 class 实现继承时,如果子类中想要定义同名函数重载基类中的函数,这个函数必须加上关键词 override ,以确保基类中的同名函数保持同步。
noImplicitReturns(released 1.8)
no implicit returns 没有隐含的 return 。
当启用时,将会检查函数中的所有代码路劲,以确保它们返回一个值。
function lookupHeadphonesManufacturer(color: "blue" | "black"): string {
if (color === "blue") {
return "beats";
} else {
"bose";
}
}
以上代码,else 语句块中无返回值,会触发如下编译错误:
Function lacks ending return statement and return type does not include 'undefined'.
noImplicitThis(released 2.0)
no implicit this 在使用 this 表达式时,如果隐含的类型是 any ,则会引发错误。
默认值跟 strict 属性相关,如果 strict 为 true ,则该属性也为 true ,反之为 false 。
例如,下面的类 Rectangle 返回一个函数,该函数试图访问 this.width 和 this.height ,但是在 getAreaFunction 的内部函数中的 this 的上下文并不是 Rectangle 的实例。
class Rectangle {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
getAreaFunction() {
return function () {
return this.width * this.height;
};
}
}
在 getAreaFunction 中返回的匿名函数中使用了 this ,此时 this 的上下文并不是 Rectangle ,所以会提示如下错误:
'this' implicitly has type 'any' because it does not have a type annotation.
noPropertyAccessFromIndexSignature(released 4.2)
no property access from index signature ,这个设置确保了通过 “.” (obj.key)语法和通过“索引”(obj["key"])语法访问字段的一致性,以及该属性在类型中的声明方式。
如果没有这个标志,typescript 将允许你通过 “.” 语法访问未定义的字段:
interface GameSettings {
// Known up-front properties
speed: "fast" | "medium" | "slow";
quality: "high" | "low";
// Assume anything unknown to the interface
// is a string.
[key: string]: string;
}
const settings = getSettings();
settings.speed;
(property) GameSettings.speed: "fast" | "medium" | "slow"
settings.quality;
(property) GameSettings.quality: "high" | "low"
// Unknown key accessors are allowed on
// this object, and are `string`
settings.username;
(index) GameSettings[string]: string
以上代码,如果启用这个标志,将会引发错误,因为未知字段使用的是 “.” 语法,而不是“索引”语法:
Property 'username' comes from an index signature, so it must be accessed with ['username'].
这个标志的目的是为了在调用语法中表明对该属性存在的确定程度,如果是未明确定义的属性,使用“索引”语法访问,而不是 “.” 语法。
noUncheckedIndexedAccess(released 4.1)
typescript 有一种方式可以描述对象,对象的 key 是未知的,但 value 是已知的,这就是通过索引签名来实现的。
interface EnvironmentVars {
NAME: string;
OS: string;
// Unknown properties are covered by this index signature.
[propName: string]: string;
}
declare const env: EnvironmentVars;
// Declared as existing
const sysName = env.NAME;
const os = env.OS;
const os: string
// Not declared, but because of the index
// signature, then it is considered a string
const nodeEnv = env.NODE_ENV;
const nodeEnv: string
如果开启这个选项,将会给类型中任何未声明的字段添加 undefined 。
declare const env: EnvironmentVars;
// Declared as existing
const sysName = env.NAME;
const os = env.OS;
const os: string
// Not declared, but because of the index
// signature, then it is considered a string
const nodeEnv = env.NODE_ENV;
const nodeEnv: string | undefined
通过以上两段代码的对比可以看到,os 变量有明确定义,所以不论是否开启该选项,编译时都将该类型理解成 string ,而 nodeEnv 是通过签名的方式定义的,所以开启该选项后,未明确定义的字段将添加一个 undefined 。
这样避免因为无用未声明字段而导致的潜在错误。
noUnusedLocals(released 2.0)
no unused locals ,如果存在定义但未使用的局部变量,将会报告错误。
const createKeyboard = (modelID: number) => {
const defaultModelID = 23;
// error:'defaultModelID' is declared but its value is never read.
return { type: "keyboard", modelID };
};
noUnusedParameters(released 2.0)
no unused parameters ,如果在函数中,存在未使用的参数,将会报告错误。
const createDefaultKeyboard = (modelID: number) => {
// error:'modelID' is declared but its value is never read.
const defaultModelID = 23;
return { type: "keyboard", modelID: defaultModelID };
};
strict(released 2.3)
该选项启用了一系列类型检查行为,这些行为可以更好地保证程序的正确性,开启这个标志相当于启用了所有严格模式系列选项,这些选项在下文进行了概述。不过,你可以根据需要关闭单个严格模式选项。
未来的 typescript 版本可能会在这个选项下引入更严格的类型检查,所以升级 typescript 可能会在你的程序中产生新的类型错误,在适当和可能的情况下,将添加新的标志来禁用这种行为。
相关联的选项:
- alwaysStrict
- strictNullChecks
- strictBindCallApply
- strictFunctionTypes
- strictPropertyInitialization
- noImplicitAny
- noImplicitThis
- useUnknownInCatchVariab
strictBindCallApply(released 3.2)
strict 开启后,默认值为 true 。
启用该选项,typescript 将会检查函数的内置方法 call ,bind ,apply 是否用正确的参数调用底层函数。
// With strictBindCallApply on
function fn(x: string) {
return parseInt(x);
}
const n1 = fn.call(undefined, "10");
const n2 = fn.call(undefined, false);
// error: Argument of type 'boolean' is not assignable to parameter of type 'string'.
否则,这些函数可以接受任意参数并且返回任意类型。
// With strictBindCallApply off
function fn(x: string) {
return parseInt(x);
}
// Note: No error; return type is 'any'
const n = fn.call(undefined, false);
在 JavaScript 中,call ,bind ,apply 可以改变函数的 this 值,并以不同的方式传递参数给函数,然而这些方法在默认情况下,并不检查传递给函数的参数是否符合底层函数的参数类型。
启用这个选项,可以避免传递错误参数类型而导致的错误。
strictFunctionTypes(released 2.6)
strict 开启后,默认值为 true 。
启用这个标志时,函数的参数将会被更严格的检查。
这里有一个例子,当为启用 strictFunctionTypes 不会检查到错误:
function fn(x: string) {
console.log("Hello, " + x.toLowerCase());
}
type StringOrNumberFunc = (ns: string | number) => void;
// Unsafe assignment
let func: StringOrNumberFunc = fn;
// Unsafe call - will crash
func(10);
如果 strictFunctionTypes 启用,错误将被检测到:
function fn(x: string) {
console.log("Hello, " + x.toLowerCase());
}
type StringOrNumberFunc = (ns: string | number) => void;
// Unsafe assignment is prevented
let func: StringOrNumberFunc = fn;
// 以下为 error 信息:
Type '(x: string) => void' is not assignable to type 'StringOrNumberFunc'.
Types of parameters 'x' and 'ns' are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
在开发这个特性的过程中,typescript 官方发现很多不安全的 class 层次结构,包括一些 DOM 在内。因此,这个特性只适用函数语法(function syntax) 编写的函数,而不适用于方法语法(method syntax) :
type Methodish = {
func(x: string | number): void;
};
function fn(x: string) {
console.log("Hello, " + x.toLowerCase());
}
// Ultimately an unsafe assignment, but not detected
// 此时类型无法完全匹配上,但即使开启该选项,也无法检测到错误
const m: Methodish = {
func: fn,
};
m.func(10);
strictNullChecks(released 2.0)
strict 开启后,默认值为 true 。
当 strictNullChecks 设置为 false 时,语言实际上会忽略 null 和 undefined ,这可能导致运行时出现意外错误。
当 strictNullChecks 设置为 true 时,null 和 undefined 有自己独特的类型,如果你尝试在需要具体值的地方使用它们,你会得到一个类型错误。
例如,在这段 typescript 代码中,users.find 不能保证它一定会找到一个 user ,但在编写代码,直接用 loggedInUser.age ,好像 loggedInUser 一定存在,有经验的程序员应该知道,这样写在某些情况会报错:
declare const loggedInUsername: string;
const users = [
{ name: "Oby", age: 12 },
{ name: "Heera", age: 32 },
];
const loggedInUser = users.find((u) => u.name === loggedInUsername);
console.log(loggedInUser.age);
如果此时将 strictNullChecks 设置为 true ,将会引发一个错误,因为在使用 loggedInUser 之前,我们不确定它是否存在。
declare const loggedInUsername: string;
const users = [
{ name: "Oby", age: 12 },
{ name: "Heera", age: 32 },
];
const loggedInUser = users.find((u) => u.name === loggedInUsername);
console.log(loggedInUser.age);
// error:'loggedInUser' is possibly 'undefined'.
在 typescript 开启这个选项,编译阶段就能直接发现不严谨的代码,从而避免运行时出现意外的错误。
strictPropertyInitialization(released 2.0)
strict 开启后,默认值为 true 。
当该选项设置为 true ,如果 class 中声明了某个属性,却没在构造函数中赋值,将会引发编译错误。
class UserAccount {
name: string;
accountType = "user";
email: string;
// error:Property 'email' has no initializer and is not definitely assigned in the constructor.
address: string | undefined;
constructor(name: string) {
this.name = name;
// Note that this.email is not set
}
}
Try
在上面例子中:
this.name 在构造函数中赋值了;
this.accountType 设置了默认值;
this.email 既没默认值,也没在构造函数中赋值,将会引发错误;
this.address 声明时定义值能为 undefined ,这意味着不必非得设置它;
useUnknownInCatchVariables(released 4.4)
strict 开启后,默认值为 true 。
use unknown in catch variables ,在 typescript 4.0 中新增的属性,允许 catch 子句中将变量的类型从 any 更改为 unknown 。这样,你就可以编写如下代码:
try {
// ...
} catch (err) {
// We have to verify err is an
// error before using it as one.
if (err instanceof Error) {
console.log(err.message);
}
}
这种模式确保错误处理代码更加全面,因为我们无法提前保证被抛出的对象是 Error 子类。如果启用了 useUnknownInCatchVariables 标志,就不需要额外的语法(: unknown)或者一个 linter 规则来强制执行这种行为。
简单来说,这个特性让我们在处理错误时更加严谨,因为我们不能假设被抛出的对象一定是某种特定类型的错误。使用 unknown 类型可以迫使你在处理错误时进行更全面的检查。