我以为 TypeScript 会帮我挡住所有 undefined,结果一个标注成 string 的值运行时是 undefined、访问属性直接崩,编译器却一声不吭,我排查了大半天的复盘

我一直以为用了 TypeScript、变量都标了类型,就再也不怕 undefined 了。结果线上某函数参数标的是 name: string,运行时实际传进来却是 undefined,name.toUpperCase() 直接崩,而编译完全通过、一个警告都没给。深挖才懂是项目没开 strictNullChecks(或整个 strict):非严格模式下 null 和 undefined 被当成"所有类型的合法值",string 类型合法地可以是 abc、也可以是 null/undefined,所以把 undefined 赋给 string 参数 TS 认为完全合规、不警告,直到运行时才崩。"用了 TS 就自动有空安全"是危险误解——它的空值检查必须显式开 strictNullChecks 才生效。这篇从 strictNullChecks 与 strict 模式讲起,到开启 strict+类型诚实标 | null+可选链?./空值合并??的正解、老项目渐进式迁移、?? 和 || 的关键区别、strict 还挡住的其他坑、strict 开启前后同段代码的天壤之别,以及那句最戳心的——工具能为你做什么和默认会为你做什么不是一回事,别让它的强大沉睡在默认配置里,编译期多流的汗是运行期少流的血。

我以为 TypeScript 会帮我挡住所有 undefined,结果一个标注成 string 的值运行时是 undefined、访问属性直接崩,编译器却一声不吭,我排查了大半天的复盘

这是一个让我对 TypeScript 的"空安全"重新认识的故事。我一直觉得,用了 TypeScript,就再也不用怕 undefined——我的变量都规规矩矩地标了类型,比如 let name: string,编译器应该会帮我把所有可能为空的情况都挡住。可现实给了我一巴掌:线上某个函数,参数标注的是 name: string,可它运行时实际传进来的,竟然是 undefined!代码里一句 name.toUpperCase(),直接抛出 Cannot read properties of undefined (reading 'toUpperCase') 崩溃了。而最让我费解的是——这段代码,编译完全通过、编译器一个警告都没给!我明明标了 string,undefined 是怎么混进来、还骗过了类型检查的?

我顺着这个矛盾深挖,才终于揭开真相,补上了我对 TypeScript 一个最关键的认知漏洞:问题的核心,是我的项目没有开启 strictNullChecks(或整个 strict 模式)。我一直想当然地以为,"TypeScript 默认就会做严格的空值检查";可真相是:非严格模式(strictNullChecks: false)下,TypeScript 对 nullundefined 的处理,是极其宽松的——nullundefined,被当成了"所有类型的合法值"!也就是说,在非严格模式下,一个 string 类型的变量,合法地可以是 "abc",也可以是 null,还可以是 undefined——TypeScript 认为这完全没问题,自然不会给你任何警告。所以,当我的 name: string 实际收到一个 undefined 时,编译器压根不觉得这是错误(因为在它眼里,undefined 本来就是 string 的合法值),于是放行;直到运行时,真的去 undefined.toUpperCase(),才轰然崩溃。我这才痛彻地明白:"用了 TypeScript,就自动有了空安全"是一个危险的误解;TypeScript 强大的"空值检查"能力,必须显式地开启 strictNullChecks 才会生效;而在它关闭时,整个类型系统对 null/undefined 几乎是"视而不见"的,你以为的"类型保护",在空值这个最常见的崩溃源面前,形同虚设。要真正享受到 TypeScript 的安全,第一步,也是最重要的一步,就是开启 strict 模式——否则,你只是用着 TypeScript 的语法,却没拿到它最核心的安全保障

故障现场:非严格模式下,undefined 骗过了类型检查

我把这个"undefined 混进 string"的现场,摊开给你看:

// ✗ 灾难: tsconfig 没开 strictNullChecks, null/undefined 是所有类型的合法值
// tsconfig.json:
//   { "compilerOptions": { "strictNullChecks": false } }   // ✗ 或干脆没开 strict

function greet(name: string) {
    return name.toUpperCase();   // ✗ 编译通过! 但 name 可能是 undefined
}

// 调用处:
const user = getUser();          // 假设它可能返回 { name?: string }
greet(user.name);                // ✗ user.name 是 undefined, 但编译不报错!
// 运行时: Cannot read properties of undefined (reading 'toUpperCase') → 崩!

// 为什么编译器不报错?
//   - strictNullChecks: false 时, null 和 undefined 是"所有类型的子类型"。
//   - 即: string 类型, 合法地可以是 "abc" / null / undefined。
//   - 所以把 undefined 赋给 string 参数, TS 认为完全合规 → 不警告。

// 对比: strictNullChecks: true 时
//   function greet(name: string) {...}
//   greet(undefined);   // ✓ 编译报错: Argument of type 'undefined' is
//                       //   not assignable to parameter of type 'string'
//   → 严格模式下, undefined 不再是 string 的合法值, 当场拦住!

// 这意味着非严格模式下:
//   - let s: string = null;        // ✗ 不报错(本不该允许)
//   - obj.maybeNull.foo;           // ✗ 不报错(可能 null.foo 崩溃)
//   - 函数返回可能 undefined, 调用方拿来就用, 全程无警告。
//   → TS 对空值"睁一只眼闭一只眼", 把最常见的崩溃源放行了。

// 根因: 没开 strictNullChecks, null/undefined 被当成所有类型的合法值,
//   TS 放弃了空安全检查, undefined 骗过类型检查、运行时才崩。

看着这段"编译通过却运行时崩溃"的代码,我才算彻底想明白了根源。问题的核心,是我的 tsconfig 没开 strictNullChecks——这时,nullundefined 是"所有类型的子类型",即 string 类型合法地可以是 "abc" / null / undefined所以,把 undefined 赋给 string 参数,TS 认为完全合规、不警告;直到运行时 undefined.toUpperCase() 才崩。对比一下:strictNullChecks: true 时,greet(undefined)编译报错("undefined 不能赋给 string 参数"),当场就被拦住这意味着,非严格模式下,一系列本该报错的写法全被放行:let s: string = null 不报错、obj.maybeNull.foo 不报错(可能 null.foo 崩)、函数返回可能 undefined 而调用方拿来就用全程无警告——TS 对空值"睁一只眼闭一只眼",把最常见的崩溃源放行了归根结底:没开 strictNullChecks,null/undefined 被当成所有类型的合法值,TS 放弃了空安全检查,undefined 骗过类型检查、运行时才崩——这,就是根源。

第一件事:搞懂 strictNullChecks 与 strict 模式

定位到根源,我必须把 strictNullChecksstrict 模式从根上彻底搞清楚:

strictNullChecks: TS 空安全的总开关; 关了 null/undefined 畅通无阻

# 两种模式下, null/undefined 的待遇天差地别:
# 关闭(strictNullChecks: false, 老项目/默认 tsconfig 可能没开):
#   - null/undefined 是"所有类型"的合法值。
#   - string 可以是 null; number 可以是 undefined ... 全都不报错。
#   - TS 对空值几乎"无感", 你失去了最重要的一层保护。

# 开启(strictNullChecks: true):
#   - null/undefined 是"独立的类型", 不再是别的类型的合法值。
#   - 想允许为空, 必须显式声明: string | null / string | undefined。
#   - 访问可能为空的值前, TS 强制你先"判空"(收窄), 否则编译报错。
#   → 这才是 TS"空安全"的精髓: 把空值问题, 从运行时提前到编译时。

# strict 是个"总开关", 一次性打开一组严格检查, 含:
#   - strictNullChecks    ← 本文重点(空安全)
#   - noImplicitAny       (禁止隐式 any)
#   - strictFunctionTypes / strictBindCallApply / ...
#   - alwaysStrict 等
#   → 推荐直接开 "strict": true, 拿到全套保护。

# 开启后, 你会被迫处理空值(这正是好事!):
#   function greet(name: string | undefined) {
#     if (name === undefined) return "";   // ✓ 必须先判空
#     return name.toUpperCase();           // ✓ 这里 TS 知道 name 一定是 string
#   }

# 关键认知: TS 的安全不是"免费默认送的", 是"开了 strict 才有的"。
#   - 没开 strict 的 TS, 约等于"带类型注解的 JS", 安全性大打折扣。

# 核心: strictNullChecks 是空安全总开关, 关闭时 null/undefined 畅通无阻、TS 形同虚设;
#   开启(连同 strict)才把空值问题从运行时提前到编译时, 强制你判空。

原理终于清晰了。两种模式下,null/undefined 的待遇天差地别:关闭时(老项目/默认 tsconfig 可能没开),null/undefined 是"所有类型"的合法值,string 可以是 nullnumber 可以是 undefined,全都不报错,TS 对空值几乎"无感";开启时,null/undefined 是"独立的类型"、不再是别的类型的合法值,想允许为空必须显式声明 string | null,而且访问可能为空的值前,TS 强制你先"判空"(收窄),否则编译报错——这才是 TS 空安全的精髓:把空值问题,从运行时提前到编译时strict 是个"总开关":它一次性打开一组严格检查,strictNullChecks(本文重点)、noImplicitAny(禁止隐式 any)、strictFunctionTypes 等——推荐直接开 "strict": true,拿到全套保护开启后,你会被"强迫"处理空值(这正是好事):访问 name 前必须先 if (name === undefined) 判空,判空之后,TS 才知道 name 一定是 string、让你安全访问。由此,我刻下一个关键认知:TS 的安全不是"免费默认送的",是"开了 strict 才有的";没开 strict 的 TS,约等于"带类型注解的 JS",安全性大打折扣。归根结底:strictNullChecks 是空安全总开关,关闭时 null/undefined 畅通无阻、TS 形同虚设;开启(连同 strict)才把空值问题从运行时提前到编译时、强制你判空。

第二件事:正解——开启 strict + 用好判空语法

搞懂了原理,正解就清晰了:开启 strict(含 strictNullChecks),然后用 类型上显式标注可空 + 可选链/空值合并/类型收窄来安全地处理空值。

// ✓ 正解一: 开启 strict(tsconfig.json)
// {
//   "compilerOptions": {
//     "strict": true               // ✓ 一键开启全套严格检查(含 strictNullChecks)
//     // 或单独: "strictNullChecks": true
//   }
// }

// ✓ 正解二: 类型上"诚实地"标注可空, TS 就会强制你处理
function greet(name: string | undefined) {   // ✓ 明确它可能没有
    // ✓ 类型收窄: 判空后 TS 知道 name 一定是 string
    if (!name) return "";
    return name.toUpperCase();                // ✓ 安全, TS 不报错
}

// ✓ 正解三: 可选链 ?. —— 安全访问可能为空的属性
const city = user?.address?.city;            // ✓ 任一环为空, 整体得 undefined, 不崩
//   等价于: user && user.address && user.address.city

// ✓ 正解四: 空值合并 ?? —— 为空时给默认值(注意和 || 的区别!)
const count = data.count ?? 0;               // ✓ 仅当 null/undefined 才用 0
//   ✗ 别用 ||: data.count || 0 会把 0、""、false 也当"空"替换掉! (这是另一个坑)

// ✓ 正解五: 非空断言 x! —— 你确定不为空时(慎用, 它只是骗编译器)
const el = document.getElementById("app")!;  // 你确定它存在 → 但错了运行时仍崩
//   ⚠ ! 不做运行时检查, 只是告诉编译器"信我", 滥用 = 又回到不安全。

// 开启 strict 后处理空值的常见组合:
//   - 能给默认值: x ?? default
//   - 链式访问: a?.b?.c
//   - 必须存在: 先判空 return/throw, 之后放心用(类型收窄)
//   - 真的确定: x!(但优先用判空, 别滥用 !)

// 核心: 开 strict 拿到空安全; 类型诚实标注 | null/undefined, 用 ?. / ?? / 判空收窄
//   安全处理空值; 慎用非空断言 ! (它不做运行时检查)。

修复的方向,是"开启保护 + 善用语法"正解一,开启 strict:在 tsconfig.json 里设 "strict": true(一键开启全套严格检查,含 strictNullChecks)——这是最根本的一步开启后,用这些语法安全处理空值:正解二,类型上"诚实地"标注可空(name: string | undefined),TS 就会强制你判空;判空之后,类型收窄,TS 知道它一定是 string、让你安全访问。正解三,可选链 ?.:user?.address?.city,任一环为空,整体得 undefined、不崩正解四,空值合并 ??:data.count ?? 0,仅当 null/undefined 才用默认值——注意别用 ||!data.count || 00""false 也当"空"替换掉(这是另一个经典坑)。正解五,非空断言 x!:你确定不为空时用——但要慎用,它不做运行时检查、只是骗编译器"信我",滥用就又回到了不安全归根结底:strict 拿到空安全;类型诚实标注 | null/undefined,用 ?. / ?? / 判空收窄安全处理空值;慎用非空断言 !(它不做运行时检查)。

第三件事:给老项目开启 strict 的迁移策略

道理我懂了,但老项目突然开 strict,可能瞬间冒出几百上千个报错。这次踩坑后,我也摸索出一套平滑迁移的策略:

老项目开启 strict 的迁移: 渐进式, 别想一口吃成胖子

# 直接开 strict: true 的问题:
#   - 老项目瞬间报几百上千个错, 根本改不完, 容易直接放弃。

# 渐进式策略(从松到严, 逐步收紧):
# 1. 先单独开 noImplicitAny(影响相对小), 消化掉隐式 any。
# 2. 再单独开 strictNullChecks(空安全, 收益最大但报错也多)。
#    - 配合 IDE/编译器, 逐个文件/模块修复。
# 3. 最后整体切到 strict: true。

# 工具辅助:
#   - 用 "// @ts-expect-error" / "// @ts-nocheck" 临时压制个别文件,
#     先让项目编译通过, 再排期逐步清理(别滥用, 要留 TODO)。
#   - ts-strictify / typescript-strict-plugin 等工具可"只对新文件强制 strict"。

# 新老分治:
#   - 新写的文件/模块, 一律 strict(从源头不再欠新债)。
#   - 老代码, 排期逐步迁移; 改到哪个老文件, 顺手把它 strict 化。

# 心态:
#   - 那几百个报错, 不是"strict 制造的问题", 而是"strict 揭露的、本就存在的隐患"。
#   - 每修一个, 就消除一个潜在的线上 undefined 崩溃。

# 核心: 老项目开 strict 别一步到位, 渐进式(先 noImplicitAny 再 strictNullChecks
#   最后 strict)、新文件先严、老代码排期迁移; 报错是揭露隐患不是制造问题。

关于老项目迁移,我也有了实战的体会。直接 strict: true 的问题,是老项目瞬间报几百上千个错、根本改不完,容易直接放弃所以要渐进式:第一步,先单独开 noImplicitAny(影响相对小),消化隐式 any;第二步,再单独开 strictNullChecks(空安全,收益最大但报错也多),配合 IDE 逐个文件/模块修复;第三步,最后整体切到 strict: true还有工具辅助:// @ts-expect-error / // @ts-nocheck 临时压制个别文件、先让项目编译通过、再排期逐步清理(别滥用、留 TODO);用 typescript-strict-plugin 等工具只对新文件强制 strict核心是新老分治:新写的文件一律 strict(从源头不欠新债)、老代码排期迁移、改到哪个老文件就顺手把它 strict 化而最重要的是心态:那几百个报错,不是"strict 制造的问题",而是"strict 揭露的、本就存在的隐患";每修一个,就消除一个潜在的线上 undefined 崩溃归根结底:老项目开 strict 别一步到位,渐进式(先 noImplicitAny 再 strictNullChecks 最后 strict)、新文件先严、老代码排期迁移;报错是揭露隐患、不是制造问题。

下面这张图,是这次"undefined 骗过类型检查"的成因与解法:

第四件事:?. 、?? 、|| 、! 几个空值相关操作的对照

开启 strict 后,处理空值要频繁用到几个操作符,它们很像、却各有陷阱。我整理成一张对照表。

操作 作用 触发条件 注意
a?.b 安全访问属性 a 为 null/undefined 时短路返回 undefined 只挡 null/undefined, 不挡其他
a ?? b 空值合并(给默认值) 仅 a 为 null/undefined 时取 b 0/""/false 不会被替换 ✓
a || b 逻辑或(给默认值) a 为任何"假值"都取 b ✗ 0/""/false 也被当空替换! 易出bug
a! 非空断言 编译期: 告诉编译器"a 不为空" ✗ 不做运行时检查, 错了照样崩
a ??= b 空值合并赋值 a 为 null/undefined 才赋 b 简洁写法

这张表,把几个"长得像、行为却不同"的操作分清了。最该牢记的,是 ??|| 的区别:它们都能"给默认值",但触发条件不同——?? 只在 null/undefined才取默认值,而 || 在任何"假值"(0""false)时都取默认值!所以 count || 10 会把合法的 0 也错误地替换成 10(用户余额是 0 却显示 10),而 count ?? 10 才是对的——这是个极其常见、又极其隐蔽的 bug其余的:?. 安全访问(只挡 null/undefined 短路)、! 非空断言(只骗编译器、不做运行时检查,错了照样崩,慎用)、??= 空值合并赋值(简洁写法)。它给我的启发是:这些"空值处理操作符",是 TS/现代 JS 处理 null/undefined利器,但每一个都有精确的触发边界;用错(尤其 ||?? 用),不仅没解决问题,还会引入新的、更隐蔽的 bug搞清每个操作符"到底在什么时候生效",才能真正用好它们

第五件事:strict 模式还帮你挡住的其他坑

这次让我深刻体会到 strict 的价值,我顺势把 strict 模式下、TS 还能帮你挡住的其他坑,系统梳理了一遍。

strict 子选项 挡住的坑 不开的后果
strictNullChecks null/undefined 乱赋值 undefined 运行时崩(本文)
noImplicitAny 没标类型的参数悄悄变 any 大片代码失去类型检查
strictFunctionTypes 函数参数类型不兼容的赋值 回调类型不匹配, 运行时出错
strictPropertyInitialization 类字段没初始化就用 字段是 undefined, 访问崩
strictBindCallApply bind/call/apply 参数类型错 调用参数错位无警告
noImplicitThis this 类型不明的隐患 this 指向错乱无提示

这张表,让我看清了 strict 是一整套"安全网"。它由一组子选项组成,每一个,都在帮你挡住一类常见的、会导致运行时崩溃或诡异行为的隐患:strictNullChecks 挡空值(本文)、noImplicitAny 挡"没标类型就悄悄变 any、导致大片代码失去检查"、strictPropertyInitialization 挡"类字段没初始化就用"、strictFunctionTypes 挡函数类型不兼容……它们共同的价值是:把大量"本会在运行时才暴露的错误",提前到编译时拦截——而这,正是 TypeScript 存在的全部意义:用编译期的一点"啰嗦",换运行期的大量"安心"它给我的最大启发是:不开 strict 的 TypeScript,是一种巨大的浪费——你承担了写类型注解的"成本",却没拿到它最核心的"收益"(严格的安全检查);这就好比买了一套顶级的安全防护装备,却没有把它穿戴起来、扣好每一个搭扣用 TypeScript,就请第一时间开启 strict,把它的能力完整地、毫不打折地用起来——这,是用好这门语言的第一前提

第六件事:开一个 TS 项目时,我现在会怎么决策

现在,每当我开一个 TS 项目、或接手一个老项目,脑子里都会过一遍这张决策图——核心就一问:strict 开了吗?没开就是裸奔。

这张图的灵魂,是把 strict 当成 TS 项目的"第一件事"第一问:tsconfig 开了 strict 吗?——新项目,第一时间开 strict: true(没有商量);老项目没开,就渐进式迁移(先 noImplicitAny、再 strictNullChecks、最后整体 strict),别一步到位被海量报错劝退。开了之后,处理空值的纪律是:类型诚实标注 | null/undefined、访问前判空收窄或用 ?.、给默认值用 ?? 不用 ||(防 0/空串被错误替换)、慎用非空断言 !最后,贯穿始终:新文件一律 strict,不欠新债这套判断,让我以后无论新老项目,都不会再"用着 TS 的语法、却没拿到它的安全"——核心始终是:没开 strict 的 TypeScript,就是在裸奔。

我立下的几条规矩

这场"undefined 骗过类型检查"的事故,换来了我用 TypeScript 时,刻进骨子里的几条铁律:

  1. 新项目第一件事:开 strict: true。不开 strict 的 TS 约等于带注解的 JS,安全性大打折扣——这是底线。
  2. strictNullChecks 是空安全的命脉。关了它,null/undefined 是所有类型的合法值,TS 对最常见的崩溃源视而不见。
  3. 可空就诚实标注 | null/undefined。别假装它不会为空;标了 TS 才会强制你判空,把崩溃挡在编译期。
  4. 给默认值用 ?? 不用 ||。|| 会把 0、""、false 也当空替换,?? 只在 null/undefined 时生效,别让合法的 0 被吃掉。
  5. 慎用非空断言 !。它只骗编译器、不做运行时检查;能判空就判空,别用 ! 把刚获得的安全又丢掉。
  6. 老项目渐进式开 strict。先 noImplicitAny 再 strictNullChecks 最后 strict;新文件先严,老代码排期迁移。
  7. 报错是揭露隐患,不是制造问题。开 strict 冒出的每个错,都是一个潜在的线上崩溃,修一个少一个。

附:strict 开启前后,同一段代码的天壤之别

口说无凭。下面这段同样的代码,在 strict 开启前后,TS 的反应天差地别——跑一遍(或在 TS Playground 切换 strict)就懂:

interface User {
  name: string;
  address?: { city: string };   // ? 表示可选, 可能 undefined
}

function describe(u: User) {
  // ===== strictNullChecks: false 时 =====
  const c1 = u.address.city;          // ✗ 不报错! 但 address 可能 undefined → 运行时崩
  const len = u.name.length;          // 若 name 实际是 undefined, 也不报错 → 崩
  return `${c1}, ${len}`;

  // ===== strictNullChecks: true 时 =====
  // const c1 = u.address.city;       // ✓ 编译报错: 'u.address' is possibly 'undefined'
  // → 强制你这样写:
  const c2 = u.address?.city ?? "未知";   // ✓ 安全: 可选链 + 空值合并
  return c2;
}

// 再看赋值:
let s: string;
// strict false: s = null;            // ✗ 不报错(本不该允许)
// strict true:  s = null;            // ✓ 报错: Type 'null' not assignable to 'string'

// 函数返回值:
function find(id: number): User | undefined {   // ✓ 诚实声明可能找不到
  return users.find(u => u.id === id);
}
const u = find(1);
// strict false: u.name;              // ✗ 不报错, u 可能 undefined → 崩
// strict true:  u.name;              // ✓ 报错, 强制你先判: if (u) u.name;

// 核心: 同一段代码, strict false 时一路绿灯却埋着运行时崩溃的雷;
//   strict true 时编译器逐个揪出"可能为空"的访问, 强制你安全处理。眼见为实。

这段对照,把开不开 strict 的差别,摆得明明白白同样是 u.address.city 这行(address 是可选的、可能 undefined):strict 关闭时,TS 一声不吭地放行,埋下一颗运行时 undefined.city 的雷;strict 开启时,TS 当场报错"u.address 可能是 undefined",强制你改成 u.address?.city ?? "未知" 这种安全写法赋值、函数返回值也一样:let s: string = null 在严格模式下会报错;find() 返回 User | undefined 时,严格模式会强制你if (u) 判空才能访问 u.name这一行行"关闭时绿灯、开启时报错"的鲜明对比,胜过千言万语:strict 关闭时的"一路绿灯",是一种虚假的安宁——它不是没有问题,而是把问题藏到了运行时;strict 开启时的"满屏报错",才是真正的保护——它把每一个潜在的崩溃,都提前揪到了你面前,逼你当场解决这,正是我想用这段代码,留给每一个 TS 开发者的最后一课:别害怕 strict 带来的报错——那一个个红色的波浪线,不是 TS 在"为难你",而是它在"替你挡子弹";编译期多流的每一滴汗,都是运行期少流的一滴血。把 strict 开起来,让 TypeScript 真正成为你的盔甲,而不只是一件好看的外套

写在最后

回头看,这场由"没开 strictNullChecks"引发的事故,真正教给我的,是一个比"记得开 strict"本身更深的道理:很多强大的工具,它的"能力"和它的"默认配置",并不是一回事;一个工具为你做什么,和它默认会为你做什么,常常隔着一个需要你主动去开启的"开关";而不去了解、不去开启这些开关,你就只用到了它一小部分的能力,甚至误以为"它就这点本事"我曾笃信"TypeScript = 类型安全",却从未想过,这份"安全",原来是有前提、有开关;我享受着写类型注解的"麻烦",却因为没开 strict,而根本没拿到那份"麻烦"本应换来的"安全"回报——这是何等的得不偿失。这让我深刻地意识到:用好任何一个工具,都不能停留在"会用它的基本功能",更要去探究"还能为我做什么、这些能力怎么开启、配置项各意味着什么";把工具的能力,完整地、用对配置地发挥出来很多时候,性能、安全、可靠性的巨大差距,不在于"用没用某个工具",而在于"有没有把这个工具用对、用满";而这,往往就藏在那些被默认配置关闭着、等待你去主动开启的"开关"里去了解并用满工具的能力,别让它的强大,沉睡在默认配置里——这,是我用一次"undefined 崩溃"的事故,换来的、关于 TypeScript、也关于"如何用好工具"的、最朴素也最深刻的领悟。如果这篇复盘,能让你回去第一时间检查一下自己项目的 tsconfig 开没开 strict,那我对着那个混进来的 undefined 熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我在 C# 里把一个 LINQ 查询当结果反复用,先 Count 再 foreach,结果同一个查询被默默执行了好多遍、数据库往返暴增,我排查了大半天的复盘

2026-6-2 3:18:20

技术教程

我的 AI Agent 调工具查数据时返回了个空结果,它却当成查到了、基于这个空结果一路推理下去,最后给出一个看起来很完整其实全错的答案,我排查了大半天的复盘

2026-6-2 3:30:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索