MODO®
ISSUE 07 / 2026
04 工程手记 / Engineering

TypeScript
严格模式生存指南

把一个 any 满天飞的老项目,用三个月渐进到 strict: true,any 比率从 38% 压到 4.7%。这里是一份可复制的路线图、工具链与团队规范模板。

作者 林知远
日期 2026.07.22
时长 13 分钟
标签 TypeScript · 工程

接手这个项目时,tsconfig 里 strict: false,全仓 38% 的函数签名带 any。CI 跑 tsc --noEmit 永远通过,因为类型检查形同虚设。

三个月后,strict: true,any 比率 4.7%,剩下的都是有明确标注的逃生通道。这篇文章记录我们怎么走过来的——不是「开启开关」那么简单。

01

严格模式不是开关

strict: true 不是一个布尔位,而是一组子开关的总和。理解每一项的含义,是渐进迁移的前提:

子开关 检查什么 关掉的代价
noImplicitAny 未标注类型的参数是否隐式 any 静默吞掉错误
strictNullChecks null / undefined 必须显式声明 运行时 NPE 漏网
strictFunctionTypes 函数参数双向协变 → 逆变 回调类型不安全
strictPropertyInitialization 类属性必须初始化或断言 实例字段读到 undefined
noImplicitThis this 必须能推断 回调里 this 指向失控

一上来全开,报错可能上千条,没人能审完。正确的做法是按风险从低到高逐项开启,每开一项就清掉它带来的错误。

NOTE

strictNullChecks 是收益最高的一项。据统计,JS 运行时错误的 70% 与 null/undefined 有关。先开它,能把一类崩溃从根上堵掉。

02

渐进迁移路线

我们的迁移分四步走,每步都有明确的退出标准。强行一步到位的代价是——没人愿意审 1200 条报错。

第一步:建立基线

先量化现状,再谈目标。用 type-coverage 统计 any 比率作为基线:

# 安装并统计:被精确类型覆盖的比例
npx type-coverage --detail

# 输出示例
# 78.2% covered
# Details:
#   src/api/request.ts:15  any  (参数 event)
#   src/utils/format.ts:8  any  (返回值)

把基线写进 CI,只允许下降、不允许上升。这条规则比开启 strict 更重要——它把"恶化"变成了一次可阻断的提交。

第二步:逐项开启子开关

{
  "compilerOptions": {
    // 第一周:风险最低
    "noImplicitThis": true,
    "noUnusedLocals": true,

    // 第二周:收益最高
    "strictNullChecks": true,

    // 第三周:结构性收紧
    "noImplicitAny": true,
    "strictFunctionTypes": true,

    // 最后:类相关
    "strictPropertyInitialization": true
  }
}

每一项开启后,运行 tsc --noEmit,把报错按文件聚合,分配给最熟悉对应模块的人修。不要让一个人扛全部——他会用 @ts-ignore 蒙混过关。

第三步:清理逃生通道

迁移过程中会冒出大量 as any 和 @ts-ignore。它们是必要的过渡手段,但必须给每个加注释,标注原因:

// ✅ 标注原因:三方库类型缺失,已提 issue #1234
const editor = new MonacoEditor() as any;

// ✅ 标注原因:JSON Schema 运行时推断,待 zod 迁移后移除
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parse(raw: any): Config { /* ... */ }

// ❌ 没注释的 any 就是定时炸弹
function handle(data: any) { /* ... */ }

配合 ESLint 规则强制要求注释,能让"逃生通道"变成"已知债务",而不是隐式蔓延。

第四步:用 Zod 替换运行时 any

很多 any 不是懒,是「运行时数据形状不确定」。这类用 Zod 把推断权交给类型系统:

import { z } from 'zod';

// 定义 schema:运行时校验 + 编译期类型,一份
const ArticleSchema = z.object({
  id: z.string(),
  title: z.string(),
  tags: z.array(z.string()),
  publishedAt: z.string().datetime(),
});

// 类型自动派生,不再手写 interface
type Article = z.infer<typeof ArticleSchema>;

// 边界处校验,失败即报错,不再信任外部输入
function loadArticle(raw: unknown): Article {
  return ArticleSchema.parse(raw); // 抛错带路径
}

把"相信外部输入"变成"校验外部输入",any 就无处藏身。

— 第三周的顿悟
03

把 any 比率降下来

工具链是这次迁移最关键的杠杆。光靠人盯,三个月降不到 4.7%。

三件套

工具 职责 在 CI 的角色
type-coverage 统计 any 比率 设阈值,低于则失败
eslint-plugin-ts 禁止无注释 any 每次提交拦截
prune-unused 标记未被消费的 any 周报里列出可清理项

关键是把工具的输出变成数字,把数字变成 CI 阈值。我们的配置:

# .github/workflows/typecheck.yml
- name: Type coverage
  run: npx type-coverage --at-least 95 --detail

- name: No bare any
  run: npx eslint . --rule '@typescript-eslint/no-explicit-any: error'

- name: No ts-ignore without reason
  run: npx eslint . --rule '@typescript-eslint/ban-ts-comment: error'

三个月的 any 比率走势(每两周统计一次):

38→4.7%
any 比率
214→9
ts-ignore 数
0
运行时 NPE 周报
TIP

把比率做成看板,而不是任务清单。每周把数字贴到团队群,下降就庆祝、上升就复盘。把"质量"变成可视化的趋势,比写规范文档有效十倍。

04

TS 救不了你的时刻

严格模式挡住了很多错,但它不是银弹。下面这些场景,TS 的类型检查在编译期就放弃了对真值的判断:

结构化类型 + 运行时形状

// TS 认为这两个结构兼容,但运行时 data 可能缺字段
interface User { id: string; name: string; }
interface Admin { id: string; name: string; role: string; }

// 编译通过!但 admin 没有 role 字段
const u: User = { id: '1', name: 'a' };
const a: Admin = u; // ✅ 结构兼容,但运行时 a.role 是 undefined

这是结构化类型的本质——它看形状不看名义。边界处(API、localStorage、URL 参数)必须用 Zod 或 io-ts 校验,否则类型保证只是一厢情愿。

泛型擦除与运行时

// 泛型在运行时被擦除,is 检查没法依赖 T
function first<T>(arr: T[]): T {
  return arr[0]; // 编译通过
}

// 调用方传入空数组 → undefined,但 TS 不会警告
const x = first<number>([]); // 类型是 number,实际是 undefined
x.toFixed(); // 运行时崩溃

这类问题只能靠边界校验 + 空数组守卫,类型系统在擦除后帮不上忙。

as 断言:绕过检查的合法通道

任何 as 都是开发者向编译器签下的"我负责"声明。我们统计过:项目里 80% 的运行时类型错误,根因都能追到某个 as。给 as 加 lint 计数,是值得的长期投资。

类型系统挡住了粗心,挡不住撒谎。

— 关于 as 断言的总结
05

团队规范模板

把前面所有经验收成一份可直接抄走的团队规范。我们的版本长这样:

WARN

规范不是贴在墙上就生效的。把每一条对应到 CI 规则上,让它变成可拦截的提交,否则它只是文档里的一段话。代码质量从来靠机器守,不靠人自觉。

严格模式的真正价值不是挡住错误,而是逼团队把意图写进类型。一旦每个函数签名都能说出"我要什么、我给什么",协作成本会肉眼可见地下降。

三个月走下来,最大的收获不是 any 比率降到 4.7%,而是团队开始用类型思考——这种思维方式比任何 lint 规则都长久。

TypeScript 严格模式 工程实践 团队规范