CUGGZ / FECharge

前端进阶必备,HTML、CSS、JavaScript、TypeScript、Vue、React、Webpack等学习教程
MIT License
35 stars 2 forks source link

如何提高 TypeScript 的代码质量? #34

Open CUGGZ opened 2 years ago

CUGGZ commented 2 years ago

Typescript 是 Microsoft 开发的一种编程语言,旨在为 Javascript 语言带来严格的类型检查和类型安全方面的安全性。它是 Javascript 的超集,可以编译为 Javascript。编译选项是 tsconfig.json 文件中的属性,可以启用或禁用以改善 Typescript 体验。下面就来看看如何通过设置编译选项来提高 Typescript 代码的质量。

前言

在 TypeScript 项目中,tsconfig.json 放在项目根目录,它指定了用来编译这个项目的根文件和编译选项。在 tsconfig.json 里有以下可配置项:

{
  "compileOnSave": true,
  "files": [],
  "include": [],
  "exclude": [],
  "extends": "",
  "compilerOptions": {}
}

这些配置项的作用如下:

switch (name) { case 'Mike': console.log('name is Mike'); case 'John': console.log('name is John'); break;
}

当编译上面的代码时,将会提示以下错误:
```typescript
error TS7029: Fallthrough case in switch

发生这种情况是因为在第一个 switch 语句的分支中缺少了 break 关键字。

5. strictNullChecks

将这个编译选项设为 true 时,nullundefined 值不能赋值给非这两种类型的值,别的类型的值也不能赋给它们。 除了 any 类型,还有个例外就是 undefined 可以赋值给 void 类型。这个选项可以帮助我们消除 Uncaught TypeError 错误。考虑下面的例子:

let title: string;
name = title;
console.log(name);

当编译上面的代码时,将会提示以下错误:

error TS2454: Variable 'title' is used before being assigned

解决这个错误的方法是在使用变量之前为其赋值:

let title: string = "Student"
name = title
console.log(name)

6. noImplicitAny

Typescript 中的每个变量都有一个类型。我们要么显式地定义类型,要么 Typescript 推断它的类型。 考虑下面的例子:

function value(a) {
  return;
}

在上面的代码中,有一个参数为 a 的函数。由于没有为该参数定义类型,因此 Typescript 推断该参数具有 any 类型。 将此编译选项设置为 true 时,编译器会提示以下错误:

error TS7006: Parameter 'a' implicitly has an 'any' type

解决此问题的方法就是确保正确定义每个参数的类型。

7. noImplicitThis

将这个编译器选项设置为 true 时,Typescript 会在不正确地使用 this 关键字的情况下或在不清楚 this 所指的位置的地方提示错误。

class Person {
  weight: number;
  height: number;

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

  getBodyMassIndex() {
    return function () {
      return this.weight / (this.height * this.height);
    };
  }
}

由于 Javascript 中存在作用域,当编译上面的代码时,就会提示以下错误。 这是因为 this 关键字的上下文默认没有绑定到任何 Person 实例。

error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation

解决这个问题的方法就是使用箭头函数,因为箭头函数使用其父级的执行上下文:

class Person {
  weight: number;
  height: number;

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

  getBodyMassIndex() {
    return () => {
      return this.weight / (this.height * this.height);
    };
  }
}

8. strictBindCallApply

这个编译选项可以确保使用具有正确参数的 call()bind()apply() 函数。

const numHandler = (a: number) ={
  console.log(`log ${a}!`);
}
numHandler.call(undefined, 'Mike')

当把这个编译选项设置为 true 的情况下运行上述代码时,将会提示以下错误:

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

为了解决这个问题,需要传入正确的参数:

const numHandler = (a: number) ={
  console.log(`log ${a}!`)
}

numHandler.call(undefined, '25')

9. strictPropertyInitialization

将这个编译选项设置为 true 时,可以确保在构造函数中初始化所有类属性。

class User {
    name: string;
    age: number;
    occupation: string | undefined;

    constructor(name: string) {
        this.name = name;
    }
}

在上面的代码块中有一个 User 类,constructor() 方法是初始化其实例属性的地方。 当实例化一个类对象时,JavaScript 会自动调用 constructor() 方法。 Typescript 要求我们要么初始化定义的属性,要么指定一个 undefined 类型。 因此,当编译上面的代码时,将会提示以下错误:

error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

10. useUnknownInCatchVariables

在 Javascript 中,可以抛出错误并在 catch 中捕获它。 通常这将是一个 error 实例,默认设置为 any。 将 useUnknownInCatchVariable 编译选项设置为 true 时,它会隐式地将 catch 中的任何变量设置为 unknown 而不是 any。 考虑下面的例子:

try {
    throw 'myException'; 
}
catch (err) {
    console.error(err.message); 
}

当编译上述代码时,它会将 err 更改为 unknown 类型。 因此会提示以下错误;

error TS2571: Object is of type 'unknown'.

产生此错误是因为 Typescript 将 err 设置为 unkown。 可以通过以下方式来修复此错误:

interface Animal { name: string; }

interface Dog extends Animal { breeds: Array; }

let getDogName = (dog: Dog) => dog.name; let getAnimalName = (animal: Animal) => animal.name;

getDogName = getAnimalName; // Okay getAnimalName = getDogName; // Okay

上面的代码运行时并没有提示错误,默认情况下参数是双向协变比较的。 超类型 `getAnimalName` 和子类型 `getDogName` 的方法可以相互分配。 如果 `strictFunctionTypes` 设置为 `true`,则 Typescript 的参数进行逆变比较。
```typescript
//strictFunctionTypes : true
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breeds: Array<string>;
}

let getDogName = (dog: Dog) => dog.name;
let getAnimalName = (animal: Animal) => animal.name;

getDogName = getAnimalName; // Okay
getAnimalName = getDogName; // Error

当上面的代码运行时,将会提示以下错误:

Type '(dog: Dog) => string' is not assignable to type '(animal: Animal) => string'.
Types of parameters 'dog' and 'animal' are incompatible.
Property 'breeds' is missing in type 'Animal' but required in type 'Dog'.

这里,getAnimalName 是比 getDogName 更广泛的函数。 因此,在这种情况下,无法将超类型分配给子类型。 但是,可以将子类型分配给超类型。 大多数时候,函数参数应该是逆变的,而不是双向协变的。 如果启用这个编译选项,Typescript 将不会将函数参数视为双向协变。

12. allowUnreachableCode

UnReachable 的代码永远不会被执行,例如在 return 语句之后的代码。 将将这个编译选项设置为 true 时,将忽略无法访问的代码。 相比之下,TypeScript 会在 allowUnreachableCode 设置为 false 时验证我们的代码路径,确保所有代码都可以访问和使用。 设置为 true 时,如果检测到任何无法访问的代码,则会引发错误。

const randomNum = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }

  return true;
};

如果代码被证明无法访问,Typescript 将提示以下警告:

error TS7027: Unreachable code detected.

13. noPropertyAccessFromIndexSignature

当此编译选项设置为 true 时,它要求我们使用 [] 括号表示法和 . 点符号表示法访问未知属性以访问已定义的属性。 这提高了一致性,因为使用点符号访问的属性始终指示现有属性。 obj.key 语法如果属性不存在,可以使用 [] 括号表示法:obj["key"]

interface HorseRace {
  breed: "Shetland" | "Hackney" | "Standardbred";
  speed: "fast" | "slow";
  // 尚未定义的属性的索引签名
  [key: string]: string;
}
declare const pick: HorseRace;
pick.breed;
pick.speed;
pick.ownerName;

在上面的示例中,我们定义了一个 HorseRace 接口,并为其赋予了一些属性,例如breedspeed和索引签名(用于未知属性)。 当编译上面的代码时,将会提示以下错误:

error TS4111: Property 'ownerName' comes from an index signature, so it must be accessed with ['ownerName'].

要修改此错误,需要在调用属性 ownerName 时使用括号表示法:

pick.breed;
pick.speed;
pick["ownerName"]

14. exactOptionalPropertyType

默认情况下,Typescript 会忽略一个属性是否被设置为“undefined”,因为它没有被定义,或者被定义为了 undefined

//exactOptionalPropertyTypes = false

interface Test {
  property?: string;
}
const test1: Test = {};
console.log("property" in test1); //=> false

const test2: Test = { property: undefined };
console.log("property" in test2);

上面代码执行的时候不会产生错误,在 test1 中,检查是否定义了 property; 如果不是,它会打印一个 false。 在 test2 中,打印出 true,因为定义了 property 并将其设置为 undefined。 接下来,把 exactOptionalPropertyTypes 选项设置为 true:

//exactOptionalPropertyTypes = true

interface Test {
  property?: string;
}
const test1: Test = {};
console.log("property" in test1); // false

const test2: Test = { property: undefined };
console.log("property" in test2);  // true

编译上述代码时,将会提示以下错误:

error TS2375: Type '{ property: undefined; }' is not assignable to type 'Test' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'property' are incompatible. Type 'undefined' is not assignable to type 'string'.

这里,Typescript 不允许定义 undefined 的属性。 为了解决这个问题,可以用 undefined 类型定义属性。

//exactOptionalPropertyTypes = true

interface Test {
  property?: string | undefined;
}

const test1: Test = {};
console.log("property" in test1); //false

const test2: Test = { property: undefined };
console.log("property" in test2);  //true

当启用 exactOptionalPropertyTypes 时,Typescript 会意识到这两种具有未定义属性的不同方式。 它还确保如果我们想显式地将属性定义为 undefined,则必须首先使用 undefined 类型对其进行注释。

15. forceConsistentCasingInFileNames

当这个选项设置为 false 时,将遵循运行 Typescript 的操作系统 (OS) 的区分大小写规则。 它可以区分大小写(操作系统区分文件名中的小写和大写字符)或不区分大小写(操作系统不区分字符大小写)。 当 forceConsistentCasingInFileNames 选项设置为 true 时,如果尝试导入名称大小写与磁盘上文件名称大小写不同的文件,Typescript 将引发错误。

// 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);
  }
}

在不考虑文件名大小写的情况下,在上面的代码中导入 StringValidator.ts 文件。 如果 forceConsistentCasingInFileNames 选项设置为 true,将会提示以下错误:

error TS1149: File name 'C:/Users/acer/Desktop/workBase/writing/Typescript/tsc/stringValidator.ts' differs from already included file name 'C:/Users/acer/Desktop/workBase/writing/Typescript/tsc/StringValidator.ts' only in casing.

要解决此问题,需要在导入文件时确保文件名大小写正确。

16. 总结延伸

编译选项大致可以分为五类:基础选项、类型检查选项、额外检测选项、模块解析选项、Source Map 选项、实验选项。

(1)基础选项

注意: 开启了这些检查如果有错会提示但不会报错。

(4)模块解析选项

参考: