MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript严格模式四重防护机制解读

2023-08-134.8k 阅读

一、TypeScript 严格模式概述

TypeScript 是 JavaScript 的超集,它为 JavaScript 引入了静态类型检查机制,使得代码在编写阶段就能发现许多潜在的类型错误。严格模式是 TypeScript 中一组增强类型检查的选项,通过开启这些选项,可以显著提升代码的质量和稳定性,减少运行时错误的发生。

1.1 为什么需要严格模式

在传统的 JavaScript 开发中,由于其动态类型的特性,很多类型相关的错误只有在运行时才能被发现。这可能导致调试困难,特别是在大型项目中,一个小小的类型错误可能会引发一系列难以追踪的问题。例如:

function add(a, b) {
    return a + b;
}
const result = add(1, '2');

在上述代码中,add 函数本意是进行数字相加,但由于 JavaScript 是弱类型语言,传入一个数字和一个字符串时,它会执行字符串拼接操作,而这可能并非开发者的初衷,并且在编写代码时很难发现这个潜在问题。

TypeScript 的严格模式通过在编译阶段对类型进行严格检查,能够提前捕获这类错误,提高代码的健壮性。

1.2 开启严格模式

tsconfig.json 文件中,可以通过设置 strict 选项为 true 来开启严格模式。strict 选项是一个便捷的开关,它会同时开启一系列严格类型检查的子选项,包括 noImplicitAnystrictNullChecksstrictFunctionTypesstrictBindCallApply 等,这些子选项共同构成了 TypeScript 严格模式的四重防护机制。

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

二、第一重防护:noImplicitAny

2.1 什么是 noImplicitAny

noImplicitAny 选项规定,如果一个变量或函数参数没有显式指定类型,TypeScript 编译器不会默认推断其类型为 any,而是会抛出错误。这有助于避免在代码中意外使用 any 类型,因为 any 类型会绕过 TypeScript 的类型检查,使得代码失去了静态类型检查的优势。

2.2 未开启 noImplicitAny 的情况

在未开启 noImplicitAny 时,以下代码是合法的:

function printValue(value) {
    console.log(value);
}
printValue('Hello');
printValue(123);

在上述代码中,value 参数没有指定类型,TypeScript 会默认推断其类型为 any。虽然代码能够正常运行,但由于 any 类型允许任何值传入,开发者可能会在后续维护中不小心传入不恰当的值,而编译器不会发出任何警告。

2.3 开启 noImplicitAny 的情况

当开启 noImplicitAny 后,上述代码会报错:

function printValue(value) { // 报错:Parameter 'value' implicitly has an 'any' type.
    console.log(value);
}
printValue('Hello');
printValue(123);

要解决这个问题,需要显式指定 value 的类型:

function printValue(value: string | number) {
    console.log(value);
}
printValue('Hello');
printValue(123);

这样,当传入不符合指定类型的值时,编译器会报错,从而提前发现潜在的类型错误。

2.4 对函数返回值的影响

noImplicitAny 同样适用于函数返回值。如果函数没有显式指定返回类型,且返回值类型不能被明确推断,也会报错。例如:

function getValue() {
    const random = Math.random() > 0.5? 'Hello' : 123;
    return random; // 报错:Function lacks ending return statement and return type does not include 'undefined'.
}

要解决这个问题,可以显式指定返回类型:

function getValue(): string | number {
    const random = Math.random() > 0.5? 'Hello' : 123;
    return random;
}

三、第二重防护:strictNullChecks

3.1 什么是 strictNullChecks

strictNullChecks 选项开启后,TypeScript 会严格区分 nullundefined 类型。在未开启此选项时,nullundefined 可以赋值给任何类型的变量,这可能导致运行时的 nullundefined 引用错误。开启 strictNullChecks 后,只有当类型明确包含 nullundefined 时,才能进行赋值。

3.2 未开启 strictNullChecks 的情况

在未开启 strictNullChecks 时,以下代码不会报错:

let name: string;
name = null;

这里 name 声明为 string 类型,但却可以赋值为 null,如果后续代码对 name 进行字符串操作,就可能会引发运行时错误。

3.3 开启 strictNullChecks 的情况

当开启 strictNullChecks 后,上述代码会报错:

let name: string;
name = null; // 报错:Type 'null' is not assignable to type'string'.

要解决这个问题,需要明确类型包含 null

let name: string | null;
name = null;

这样,当对 name 进行操作时,就需要考虑 name 可能为 null 的情况,例如:

let name: string | null;
name = null;
if (name) {
    console.log(name.length);
}

3.4 函数参数和返回值

在函数参数和返回值中,strictNullChecks 同样发挥作用。例如:

function printLength(str: string) {
    console.log(str.length);
}
let value: string | null = null;
printLength(value); // 报错:Argument of type'string | null' is not assignable to parameter of type'string'.

正确的处理方式是:

function printLength(str: string | null) {
    if (str) {
        console.log(str.length);
    }
}
let value: string | null = null;
printLength(value);

四、第三重防护:strictFunctionTypes

4.1 什么是 strictFunctionTypes

strictFunctionTypes 选项主要影响函数类型的赋值兼容性。在 TypeScript 中,函数类型的赋值兼容性遵循一定的规则,strictFunctionTypes 对这些规则进行了更严格的限制,以确保函数类型的赋值更加安全。

4.2 函数参数的双向协变

在传统的 TypeScript 函数类型赋值中,函数参数是双向协变的。这意味着,如果一个函数类型 A 的参数类型是 string,另一个函数类型 B 的参数类型是 any,那么 A 可以赋值给 B,同时 B 也可以赋值给 A。例如:

let func1: (arg: string) => void = (arg) => console.log(arg);
let func2: (arg: any) => void = func1;
func2(123);

在上述代码中,func1 类型的函数参数为 stringfunc2 类型的函数参数为 anyfunc1 可以赋值给 func2,并且 func2 调用时传入 number 类型的值也不会报错,这可能会导致潜在的类型错误。

4.3 开启 strictFunctionTypes 的影响

当开启 strictFunctionTypes 后,函数参数变为逆变。即只能将参数类型更具体的函数赋值给参数类型更宽泛的函数,而不能反向赋值。例如:

let func1: (arg: string) => void = (arg) => console.log(arg);
let func2: (arg: any) => void;
func2 = func1; // 合法
func1 = func2; // 报错:Type '(arg: any) => void' is not assignable to type '(arg: string) => void'.

这样可以避免将参数类型宽泛的函数赋值给参数类型具体的函数,从而减少运行时因参数类型不匹配而导致的错误。

4.4 函数返回值的协变

与函数参数不同,函数返回值在 strictFunctionTypes 开启后仍然保持协变。即返回值类型更具体的函数可以赋值给返回值类型更宽泛的函数。例如:

let func1: () => string = () => 'Hello';
let func2: () => any = func1;

这里 func1 返回 string 类型,func2 返回 any 类型,func1 可以赋值给 func2,因为 stringany 的子类型,这种协变关系在 strictFunctionTypes 下仍然成立。

五、第四重防护:strictBindCallApply

5.1 什么是 strictBindCallApply

strictBindCallApply 选项用于确保 Function.prototype.bindFunction.prototype.callFunction.prototype.apply 方法的类型检查更加严格。在未开启此选项时,这些方法在使用时可能会出现类型不匹配但不报错的情况。

5.2 未开启 strictBindCallApply 的情况

在未开启 strictBindCallApply 时,以下代码不会报错:

function greet(name: string) {
    console.log(`Hello, ${name}`);
}
const boundGreet = greet.bind(null, 123); // 这里传入的参数类型与函数定义不匹配,但不报错
boundGreet();

在上述代码中,greet 函数期望一个 string 类型的参数,但在使用 bind 方法时传入了 number 类型的参数,由于未开启 strictBindCallApply,编译器不会发出警告。

5.3 开启 strictBindCallApply 的情况

当开启 strictBindCallApply 后,上述代码会报错:

function greet(name: string) {
    console.log(`Hello, ${name}`);
}
const boundGreet = greet.bind(null, 123); // 报错:Argument of type '123' is not assignable to parameter of type'string'.
boundGreet();

这样可以在编译阶段发现 bindcallapply 方法使用时的类型错误,确保函数调用的参数类型与函数定义一致。

5.4 实际应用场景

在实际开发中,strictBindCallApply 可以避免因函数上下文绑定或参数传递错误而导致的运行时错误。例如,在事件处理函数中,经常会使用 bind 方法来绑定 this 上下文和传递额外参数:

class Greeter {
    message: string;
    constructor(message: string) {
        this.message = message;
    }
    greet() {
        console.log(this.message);
    }
}
const greeter = new Greeter('Hello, world');
document.addEventListener('click', greeter.greet.bind(greeter, 123)); // 报错:Argument of type '123' is not assignable to parameter of type 'never'.

通过开启 strictBindCallApply,可以及时发现这种类型不匹配的问题,提高代码的可靠性。

六、综合应用与实践

6.1 实际项目中的应用

在一个实际的 Web 应用开发项目中,假设我们有一个用户管理模块,其中包含一个获取用户信息的函数 getUserInfo 和一个显示用户信息的函数 displayUserInfo

interface User {
    name: string;
    age: number;
}
function getUserInfo(id: number): User | null {
    // 模拟从数据库获取用户信息
    if (id === 1) {
        return { name: 'John', age: 30 };
    }
    return null;
}
function displayUserInfo(user: User) {
    console.log(`Name: ${user.name}, Age: ${user.age}`);
}
const userId = 1;
const user = getUserInfo(userId);
displayUserInfo(user); // 报错:Argument of type 'User | null' is not assignable to parameter of type 'User'.

由于开启了 strictNullChecks,编译器会提示 displayUserInfo 函数的参数类型不匹配,因为 getUserInfo 可能返回 null。我们需要对其进行处理:

interface User {
    name: string;
    age: number;
}
function getUserInfo(id: number): User | null {
    // 模拟从数据库获取用户信息
    if (id === 1) {
        return { name: 'John', age: 30 };
    }
    return null;
}
function displayUserInfo(user: User) {
    console.log(`Name: ${user.name}, Age: ${user.age}`);
}
const userId = 1;
const user = getUserInfo(userId);
if (user) {
    displayUserInfo(user);
}

同时,如果在项目中使用 bind 方法来处理事件,strictBindCallApply 也会发挥作用:

class UserDisplay {
    user: User;
    constructor(user: User) {
        this.user = user;
    }
    showUser() {
        displayUserInfo(this.user);
    }
}
const userObject = { name: 'Jane', age: 25 };
const userDisplay = new UserDisplay(userObject);
document.addEventListener('click', userDisplay.showUser.bind(userDisplay, 123)); // 报错:Argument of type '123' is not assignable to parameter of type 'never'.

通过这些严格模式选项的综合应用,可以有效减少代码中的潜在错误,提高项目的可维护性。

6.2 代码重构与优化

在对现有 JavaScript 项目进行迁移到 TypeScript 并开启严格模式的过程中,往往需要对代码进行重构和优化。例如,在一个包含大量函数调用和参数传递的模块中,开启 noImplicitAnystrictFunctionTypes 后,可能会发现许多函数参数和返回值类型不明确或不匹配的问题。

// 原始 JavaScript 代码
function calculate(a, b) {
    return a + b;
}
const result = calculate(1, '2');

迁移到 TypeScript 并开启严格模式后:

function calculate(a: number, b: number): number {
    return a + b;
}
const result = calculate(1, 2);

通过明确函数参数和返回值类型,不仅提高了代码的可读性,还增强了类型安全性。同时,strictNullChecks 可以帮助我们发现潜在的 nullundefined 引用问题,及时进行处理,优化代码逻辑。

七、严格模式的局限性与注意事项

7.1 类型定义的复杂性

虽然严格模式能够提高代码的质量和稳定性,但随着项目规模的增大,类型定义可能会变得复杂。例如,在处理复杂的数据结构或函数重载时,需要编写详细的类型声明,这可能增加代码的编写和维护成本。

function processValue(value: string | number | boolean) {
    if (typeof value ==='string') {
        // 这里需要进行字符串相关操作,可能需要进一步细化类型
    } else if (typeof value === 'number') {
        // 数字相关操作
    } else {
        // 布尔值相关操作
    }
}

在上述代码中,value 的类型是联合类型,在处理时需要根据不同的类型分支进行操作,这可能导致代码逻辑变得复杂。

7.2 与第三方库的兼容性

在使用第三方库时,可能会遇到类型定义不完整或与严格模式不兼容的情况。一些老旧的第三方库可能没有提供完善的 TypeScript 类型声明文件,或者其类型声明与严格模式的要求不符。这时可能需要手动编写类型声明文件(.d.ts)来解决类型检查问题,但这也增加了开发的工作量。

// 引入一个没有完善类型声明的第三方库
import someLibrary from 'third - party - library';
const result = someLibrary.doSomething(); // 可能会报错,因为类型不明确

7.3 性能影响

虽然 TypeScript 是在编译阶段进行类型检查,不会对运行时性能产生直接影响,但复杂的类型检查逻辑可能会导致编译时间变长。特别是在大型项目中,大量的类型定义和复杂的类型推断可能会使编译过程变得缓慢,影响开发效率。

八、结语

TypeScript 的严格模式通过四重防护机制,即 noImplicitAnystrictNullChecksstrictFunctionTypesstrictBindCallApply,为代码提供了更强大的类型检查能力。在实际开发中,合理应用这些机制可以显著减少运行时错误,提高代码的质量和可维护性。然而,我们也需要注意严格模式带来的一些局限性,如类型定义的复杂性、与第三方库的兼容性以及可能的性能影响。通过在项目中谨慎配置和使用严格模式,结合良好的代码设计和开发实践,我们能够充分发挥 TypeScript 的优势,打造更加健壮和可靠的软件项目。