TypeScript 严格模式

在上一节中,我们从全局角度学习了 tsconfig.json 的核心配置。其中,有一个配置项虽然只有短短一行,但它却决定了整个 TypeScript 编译器的 “性格” 是温顺还是严苛。那就是 "strict": true。

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

在现代前端工程化(如 Vue 和 React 的脚手架)中,"strict": true 这个选项默认都是开启的。开启它之后,TypeScript 就不再是一个简单的类型标注工具,而是一个严厉的 “代码质检员”。

实际上,strict 并不是一个单一的规则,而是一个 “总闸”。当我们把它设置为 true 时,底层会自动开启一整套严苛的子规则族群。

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

实际上,上面配置等价于:

{
    "compilerOptions": {
        "alwaysStrict": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictBindCallApply": true,
        "strictFunctionTypes": true,
        "strictPropertyInitialization": true,
        "useUnknownInCatchVariables": true
    }
}

在这一节中,我们来深度拆解日常开发中 "strict": true 最核心、也是最常报错的三大严格子规则。

注意: tsconfig.json 中,noImplicitAny、strictNullChecks、strictPropertyInitialization 这几个与 strict 是平级关系,它们都是 compilerOptions 下的布尔值(boolean)选项。

拒绝类型 “裸奔”:noImplicitAny

如果说 TypeScript 的灵魂是类型推导,那么 any 就是类型系统中的 “毒药”。在 tsconfig.json 中,如果我们设置了 "noImplicitAny": true(禁止使用隐式的 any),即:

{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

"noImplicitAny": true 的作用是:当 TypeScript 无法自动推断出一个变量的具体类型,且你又没有手动为它加上类型注解时,直接报错拦截。

比如初学者在写函数时,经常会忘记给参数加上类型。如果不开启这个选项,TypeScript 就会默默地把这个参数推断为 any。既然都是 any 了,那你为什么还要用 TypeScript 呢?对吧?

示例 1:无孔不入的隐式 any

// 错误写法:开发者忘记给形参 message 加类型了
// 如果不开启 noImplicitAny,TS 会默认它为 any,不报错。
// 一旦开启 strict,这里立刻爆红
function logMessage(message) {
    // 报错:参数 “message” 隐式具有 “any” 类型。
    console.log(message.toUpperCase());
}

// 正确写法:强迫你必须明确指定类型
function logMessageSafe(message: string) {
    console.log(message.toUpperCase());
}

分析:

在企业级代码审查(Code Review)中,显式的 any(比如 msg: any)尚且可以说是为了业务妥协的无奈之举,但 “隐式的 any” 绝对是不可原谅的低级失误。而 "noImplicitAny": true 可以完美地从根源上堵死了这个漏洞。

终结空指针异常:strictNullChecks

在 JavaScript 发展的 20 多年里,有一个坑害了无数的开发者的报错。这个 “坑” 甚至被称为 “价值十亿美元的错误”,也就是:

Uncaught TypeError: Cannot read property 'xxx' of undefined / null

其中,"strictNullChecks": true(严格空值检查)就是为了彻底终结这个报错而生的,即:

{
    "compilerOptions": {
        "strictNullChecks": true
    }
}

1. 开启前(非严格模式)

在非严格模式下,null 和 undefined 是所有类型的 “干儿子”。我们可以把 null 赋给一个 string 类型的变量,也可以去调用一个可能为 null 的对象的方法,TypeScript 压根不管。

2. 开启后(严格模式)

null 和 undefined 被彻底孤立后,它们只能被赋值给 any、unknown 或者它们自己。任何可能为空的变量在调用属性前,必须经过严格的判空处理。

示例 2:DOM 操作中的严格空值检查

// 假设我们在页面上获取一个按钮
const submitBtn = document.getElementById("submit-btn");

// 报错:对象可能为“null”。
submitBtn.click();

开启了 "strictNullChecks": true 之后,像上面这样的调用是非常危险的。因为页面上可能根本没有这个 ID,submitBtn 的推断类型是:HTMLElement | null。

这也是日常开发中触发频率最高的报错,没有之一!特别是在做后端接口数据渲染时,后端返回的数据随时可能缺失。"strictNullChecks": true 会强迫你在开发阶段就把所有潜在的空指针异常全部处理掉,极大提升了代码在生产环境下的健壮性。

正确的处理方式有以下 2 种:

// 方式 1:使用类型保护 (if 判空)
if (submitBtn) {
    submitBtn.click();    // 在这里,TS 知道它绝对不是 null
}

// 方式 2:使用 ES6+(ES2020) 的可选链操作符 (?.)
submitBtn?.click();

告别指向不明的 this:noImplicitThis

在 JavaScript 中,函数内部的 this 指向是非常灵活(甚至可以说是混乱)的。如果开启了 "noImplicitThis": true,TypeScript 就会严格审查函数内部的 this。如果它无法明确推断出 this 到底指向谁,就会直接报错。

{
    "compilerOptions": {
        "noImplicitThis": true
    }
}

假设我们要在页面上给一个按钮绑定点击事件,初学者经常会写出下面这样的代码:

示例 3:回调函数中的隐式 this

class App {
    public appName: string = "绿叶网";

    public init() {
        // 报错:“this” 隐式具有类型 “any”,因为它没有类型注释。
        document.getElementById("btn")?.addEventListener("click", function() {
            console.log("当前应用名称是:", this.appName); 
        });
    }
}

分析:

在上面的代码中,由于我们使用了普通的 function(),当按钮被点击时,函数内部的 this 实际上指向了那个 DOM 元素(按钮),而不是 App 类的实例。TypeScript 敏锐地察觉到了这个潜在的运行时错误,并在编译阶段就用红线将其拦截。

在现代前端开发中,解决这种 this 指向丢失的最优雅、也是最推荐的做法,就是使用箭头函数(因为箭头函数没有自己的 this,它会向外层寻找):

class App {
    public appName: string = "绿叶网";

    public init() {
        // 使用箭头函数,this 完美指向 App 实例,不再报错
        document.getElementById("btn")?.addEventListener("click", () => {
            console.log("当前应用名称是:", this.appName); 
        });
    }
}

严防未初始化的属性:strictPropertyInitialization

在进行面向对象编程时,很多小伙伴会习惯先声明类的属性,然后再去其他方法里给它赋值。这在 TypeScript 严格模式下是绝对不被允许的。

如果设置了 "strictPropertyInitialization": true,也就是:

{
    "compilerOptions": {
        "strictPropertyInitialization": true
    }
}

此时表示:类中声明的所有非可选属性,要么在声明时直接赋予初始值,要么在构造函数(constructor)中进行初始化。

示例 4:类属性的严格初始化

class User {
    // 报错:属性 “username” 没有初始化表达式,且未在构造函数中明确赋值。
    // public username: string;

    // 正确写法 1:声明时直接给初始值
    public role: string = "Admin";

    // 正确写法 2:在 constructor 中赋值
    public email: string;
    
    // 正确写法 3:如果这个属性确实要稍后由框架(如 Vue/React)来注入
    // 你可以使用非空断言操作符 "!",告诉 TS:“别管了,我保证它到时候绝对有值!”
    public token!: string;

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

老项目如何迁移到严格模式?

在新项目中无脑开启 "strict": true 是一件很爽的事情。但是,当我们接手了一个几十万行代码的 “屎山级” 老项目(以前没开严格模式),如果直接设置 "strict": true,可能就会立马直接给你弹出了 5000 多个红色报错……。

对于这种老项目,大厂通用的 “渐进式迁移策略” 如下:

1. 关闭总闸,开启分闸

我们不要一上来就直接用 "strict": true,而是在 tsconfig.json 中把它设为 false,然后手动把子规则一个个列出来并设为 true。

{
    "compilerOptions": {
        "strict": false,
        "noImplicitAny": true,       // 先只解决隐式 any 问题
        "strictNullChecks": false    // 空值问题太多,先放着
    }
}

2. 逐个击破,圈地自萌

等全组人员把 noImplicitAny 的报错全改完后,再把 strictNullChecks 改为 true。通过这种小步快跑的方式,慢慢把历史包袱消化掉。

3. 善用 @ts-expect-error 和 any 止血

如果某个“祖传”文件的报错实在太多、逻辑太复杂不敢乱改,我们可以在报错的上一行加上 // @ts-expect-error 强行忽略,或者用显式的 any 断言包裹,先保证项目能跑起来,以后再重构。

// @ts-expect-error
const legacyData: string = getOldDataFromWindow();
给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号