1、JavaScript 简史与痛点回顾

JS 从“网页里写点小脚本”一路进化到支撑数十万行的复杂前端与 Node.js 服务端应用。早期设计的“轻快”带来了不少怪癖,规模一大就变成维护成本:

// 1. == 会做隐式类型转换,结果常常反直觉
"" == 0          // true
false == []      // true
0 == '0'         // true

// 2. 链式比较不是你以为的数学意义
let x = 42
1 < x < 3        // true,对任何 x 都为 true(因为 (1 < x) 先变成 true,再被转成 1,1 < 3)


// 3. 属性拼写错误,运行期才发现
const rect = { width: 10, height: 15 }
const area = rect.width * rect.heigth  // NaN(heigth 拼错)

小项目里这些“惊喜”尚能兜住;当代码基数上万,问题就会指数级放大。我们需要在运行前把这类错误挡在门外。

2、TypeScript 是什么(以及不是什么)

  • 静态类型检查器:在执行前,基于“值的种类”(类型)检查错误。
  • JavaScript 的带类型超集:所有合法 JS 代码,放进 TS 里语法上都是合法的。
  • 不改运行时语义:TS 不改变 JS 的运行时行为;除以 0 依然是 Infinity,不会因为“有类型”就抛异常。
  • 类型擦除:编译后产出的是没有类型信息的纯 JS;类型只在编译阶段起作用。
  • 没有额外运行时库:你用的仍是 JS 的标准库与生态。

为什么这很重要?
这保证了“渐进式迁移”可行:你可以边写边引入类型,不用担心“换语言”带来的运行时差异。

3、TS 与 JS 的关系:语法、类型、运行时

3.1 语法:JS 语法在 TS 中全部合法

// 这段 JS 放进 .ts 文件也能通过语法检查(可能有类型错误,但语法没问题)
const msg = "hello"
console.log(msg)

3.2 类型:在“如何使用值”上增加规则

// 属性拼写:TS 会在编译期报错
const rect = { width: 10, height: 15 }
const area = rect.width * rect.heigth
//               ~~~~~~             ~~~~~~
// Property 'heigth' does not exist... Did you mean 'height'?

// 算术:数组参与除法没有意义
console.log(4 / [])
// The right-hand side of an arithmetic operation must be of type 'any' | 'number' | 'bigint' | enum

3.3 运行时:与 JS 一致

// 即使 TS 觉得可疑,运行时行为仍与 JS 完全一致
console.log(1 / 0) // Infinity

4、TS 到底帮你挡住了什么坑?

  • 低级错误:拼写、空指针、错误的 API 参数、算术参与者不对等。
  • 重构安全:改字段名/函数签名后,所有引用点会立刻红线
  • 接口契约:调用方与实现方的“约定”在类型层面固化,跨模块协作更稳。
  • 智能提示:IDE 补全、跳转、重命名、重构的可靠性显著提升。
  • 建模能力:用类型表达业务概念(联合、映射、条件、模板字面量类型等),让代码=文档

5、三条落地路径(从易到难)

路径 A:零成本“加固”你现有的 JS

适合:老代码、短期不便迁移、想获得基础检查与提示。

  1. .js 文件顶部加:

    // @ts-check
    
  2. JSDoc 注释补类型:

    /**
     * @param {{width: number, height: number}} rect
     */
    function area(rect) {
      return rect.width * rect.height
    }
    
  3. VS Code 会启用 TS 检查与智能提示,无需改后缀、无需编译。

路径 B:渐进式迁移到 .ts/.tsx

适合:中型项目,可分模块推进。

  • 新增 tsconfig.json,打开渐进策略(见下节)。
  • 允许现有 JS 先跑着,逐步把模块改成 .ts,为公共 API 与数据结构先写类型。
  • 单测覆盖关键路径,边迁边稳。

路径 C:新项目直接上 TS

适合:从零开始,追求最佳实践。

  • 直接用 TS 初始化(例如 Vite/Next 等脚手架的 TS 选项)。
  • 一开始就全开严格模式,后面只会更轻松。

6)可复制的 tsconfig.json 基线

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",      // 现代打包器/运行时友好
    "jsx": "react-jsx",                 // 若用 React;否则删除
    "strict": true,                     // 开启所有严格项的集合
    "noImplicitAny": true,              // 隐式 any 直接报错
    "exactOptionalPropertyTypes": true, // 可选属性的精确语义
    "noUncheckedIndexedAccess": true,   // arr[i] 可能是 undefined
    "noFallthroughCasesInSwitch": true,
    "useUnknownInCatchVariables": true,
    "skipLibCheck": true,               // 加速编译,通常安全
    "resolveJsonModule": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

如果你在做渐进迁移,可先把 strict 打开,个别地方用最小范围的 // @ts-ignoreas const/类型缩小解围,别把全局规则关掉。

7、类型工具箱:最常用、最容易“值回票价”的几招

// 7.1 基础 & 联合类型
type ID = number | string

// 7.2 可选与精确可选
type User = {
  id: ID
  name: string
  email?: string           // exactOptionalPropertyTypes = true 时更精准
}

// 7.3 unknown 优于 any(迫使你做类型收窄)
function parse(json: string): unknown {
  return JSON.parse(json)
}
const data = parse("{}")
// data.name  // ❌ 不允许直接用
if (typeof data === "object" && data && "name" in data) {
  // 经过类型守卫后才能安全使用
}

// 7.4 类型守卫(Type Guards)
function isUser(x: unknown): x is User {
  return typeof x === "object" && !!x && "id" in x && "name" in x
}

// 7.5 接口/类型别名与字面量约束
type Role = "admin" | "member"
interface Account {
  id: ID
  role: Role
}
const rbac = { admin: ["create", "delete"], member: ["read"] } as const
//           ^ 字面量推断,后续取值更精确

// 7.6 泛型(保持 API 的“可复用 + 类型安全”)
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>
  for (const k of keys) out[k] = obj[k]
  return out
}

8)把“红线”变成“绿线”:典型迁移案例

8.1 属性拼写 & 可空值

type Rect = { width: number; height: number | null }

// ✅ 标注可空,再在使用处做收窄
function area(rect: Rect) {
  if (rect.height == null) return 0
  return rect.width * rect.height
}

8.2 隐式 any → 显式类型

// ❌ 隐式 any(在 strict 下报错)
function sum(a, b) { return a + b }

// ✅ 显式类型
function sum(a: number, b: number) { return a + b }

8.3 外部数据的“防御性”建模

// 来自接口的 JSON 不可信 -> unknown
const payload: unknown = await fetch(...).then(r => r.json())
if (isUser(payload)) {
  // ✅ 通过类型守卫后再用
  console.log(payload.name)
}

9、常见坑与最佳实践

  • 永远打开 strict:早痛早治,后期收益巨大。
  • any 克制使用:真的没法标注时,用 局部 any(比如 as any)做“围栏”;更推荐 unknown + 类型守卫。
  • 先给“边界”加类型:接口响应、持久化数据、公共 API 的入参/出参——优先标注它们。
  • 利用 IDE 重构:重命名字段/函数,先让类型系统告诉你哪里会炸。
  • 别指望类型改变运行时:记住“类型擦除”的原则——运行时需要的校验(如参数必填、值域范围)仍要代码里做
  • 搭配 ESLint:用 @typescript-eslint 补齐风格与潜在逻辑问题(如未用变量、潜在 await 漏洞等)。

10、学习策略:把 JS 资源当成 TS 资源的一部分

TS 的运行时就是 JS。遇到“怎么做 X”类问题,先搜 JavaScript 的做法,通常 100% 适用;需要编译期约束时,再加上 TS 的类型标注或工具类型即可。

  • TS 不改变 JS 怎么跑,它只是在运行前为你把错拦下。
  • 从“在 JS 里加 // @ts-check + JSDoc”到“全量 TS”,任何阶段都能获得收益
  • 把类型当成团队契约与系统设计的载体,你会发现:可维护性、重构效率、跨团队协作,都会肉眼可见地提升。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐