TypeScript strict 模式:从 JS 项目逐步迁移的经验

起因

老项目 JS(或松散 TS)想拧紧 strict mode:

  • noImplicitAny
  • strictNullChecks
  • strictFunctionTypes
  • noImplicitThis
  • strictPropertyInitialization
  • alwaysStrict

一次性全开 → 几千 type error → 团队崩。
渐进 rollout 是正解。

strict 全开

{
    "compilerOptions": {
        "strict": true,
        // 等价于全开上面 6 个
    }
}

新项目直接 strict。

老项目分步骤

{
    "compilerOptions": {
        "strict": false,
        "noImplicitAny": true,
        "strictNullChecks": false,
        ...
    }
}

一个一个加,每个加完跑全 ts check + 修。

推荐顺序:

  1. noImplicitAny(多数 type 自动推或 explicit)
  2. strictNullChecks(最大改动,但最值)
  3. strictFunctionTypes
  4. 其它

strictNullChecks 影响

// 关 strictNullChecks
function getName(user) {       // user: any
    return user.name;          // 没事
}
getName(null);                  // 没报,但运行时崩

// 开
function getName(user: User | null) {
    return user.name;          // ❌ Object is possibly 'null'
}
function getNameSafe(user: User | null) {
    return user?.name;         // ✅
}

每函数 explicit handle null。
最大价值:编译期 catch null pointer 问题。

渐进:per-file strict

@ts-strict 风格 comment / 工具:

// @ts-check
// @ts-strict

或者用 ts-strict-plugin、单 file // @ts-expect-error 标技术债。

TypeScript per-file

更激进:拆 tsconfig 多个 project:

// tsconfig.strict.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": { "strict": true },
    "include": ["src/strict/**"]
}

src/strict/ 下严格,其余宽。
新代码进 strict,老代码慢慢迁。

工具帮迁移

  • ts-migrate(Airbnb 出):批量 JS → TS + 加 // @ts-expect-error
  • typescript-strict-plugin:渐进 strict per-file

渐进示例

Week 1: 装 TS, allowJs: true, 跑 baseline
Week 2: noImplicitAny on, 修 (100 error)
Week 3: 把 critical paths 改 strict (per-file)
Week 4: strictNullChecks on, 修 (500 error,多数小)
Month 2: 全 strict

常见迁移 pattern

// 之前
function process(data) {
    if (data.user.address.city) { ... }
}

// 改 1: 类型
interface User { address?: Address }
interface Address { city?: string }
function process(data: { user: User }) {
    if (data.user.address?.city) { ... }   // optional chaining
}

// 改 2: 早返
function process(data: { user: User }) {
    const city = data.user.address?.city;
    if (!city) return;
    // city 类型 narrowed string
}

optional chaining + early return 是最常用 strict-friendly pattern。

第三方 lib 没 type

// 装 @types
npm install -D @types/lodash

// 没 @types 的
declare module 'weird-lib' {
    export function doStuff(x: any): any;
}

最差情况 cast:

const lib = require('weird-lib') as any;

as any 会传染 → 限定到边界。

any vs unknown

function parse(input: any) {       // 危险
    return input.value;              // 不报,但运行时崩
}

function parse(input: unknown) {     // 强制处理
    if (typeof input === 'object' && input && 'value' in input) {
        return input.value;            // narrowed
    }
}

unknown 是 type-safe 版 any。强制 narrow 后用。
新代码用 unknown 替代 any。

strictNullChecks 后的常见错

// 1. array access
const arr: string[] = [];
const first: string = arr[0];      // ❌ undefined if empty

// noUncheckedIndexedAccess: true
const first: string | undefined = arr[0];

// 2. delete
const obj: { x?: number } = { x: 1 };
delete obj.x;                       // 必须先 optional

// 3. JSON.parse
const data: unknown = JSON.parse(s);
// 不能直接 data.x → Zod / type guard

CI gate

// package.json
"scripts": {
    "typecheck": "tsc --noEmit",
    "typecheck:strict": "tsc --project tsconfig.strict.json --noEmit"
}
# CI
- run: pnpm typecheck
- run: pnpm typecheck:strict     # 只检查 strict files

PR 改动 strict file → 必须过 strict check。
慢慢"扩 strict 边界"。

真实迁移

某老项目 (50k LOC JS):

  • 全开 strict 一次:3000 error,回归测试一周
  • 改渐进:4 个月
  • 月 1:装 TS + allowJs,0 改 → 跑通
  • 月 2:noImplicitAny + 主要路径手加 type → 50% 文件 typed
  • 月 3:strictNullChecks,整修 → 1500 changes
  • 月 4:全 strict + tslint → strict
  • 期间也持续开发新 feature

迁完收益:

  • 生产 NullPointer error 降 80%
  • IDE auto-complete 准
  • 重构信心大增
  • 新人 onboarding 快(看类型就懂 API)

skipLibCheck

{
    "skipLibCheck": true        // 第三方 .d.ts 不 check
}

大型项目几乎必开。第三方 type 偶有小问题不卡你。

不要追求 100%

any 不是恶魔。
边界(unknown API / migration code / quick prototype)用 any
// FIXME: type later
内部核心 type-tight 90% 已经收益大头。

与 jsdoc TS 对比

// JS file + JSDoc
/**
 * @param {string} name
 * @returns {string}
 */
function greet(name) {
    return `Hi ${name}`;
}

// @ts-check 让 TS check JSDoc 类型。
适合:不想转 .ts 但想要 type-check(如 lib / 小项目)。
中大型 → 直接 .ts。

踩过的坑

  1. Object.keys 返回 string[]Object.keys({a:1, b:2}).forEach((k) => obj[k]) 报 string can't index Foo。(Object.keys(obj) as (keyof Foo)[])
    cast。

  2. DOM nulldocument.querySelector('.x') 返 Element | null。
    ! non-null assert 慎用。

  3. callback this:strict 模式下 method as callback 失 this
    bind / arrow。

  4. enum 类型enum X { A, B } 编译有运行时 object。
    const X = { A: 'a', B: 'b' } as const; type X = (typeof X)[keyof typeof X]
    更轻。

  5. 复杂 generic:递归 / mapped type 编译慢。简化或拆。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。