Typescript 之 tsconfig.json 的代码检测 (一)

2 阅读14分钟

前言

用过 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 可能会在你的程序中产生新的类型错误,在适当和可能的情况下,将添加新的标志来禁用这种行为。

相关联的选项:

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 类型可以迫使你在处理错误时进行更全面的检查。