类型是给未来的自己写的文档

JavaScript 的类型错误只有在运行时才暴露,而运行时往往已经是用户手里。TypeScript 把这类问题提前到编译期,同时把「这个函数要什么、返回什么」变成可检索、可跳转、可自动补全的正式契约。它真正的收益不是少写测试,而是让重构变得可信:改一个字段名,编译器会列出所有需要跟着改的位置。

类型的价值在改动的那一刻体现——如果重构时你不敢动某个函数,说明它的约定只存在于某个人的记忆里。
四个基础心智
  • 结构类型只比较形状是否兼容,不关心来自哪个类。
  • 类型即集合联合是并集,交叉是交集,收窄是缩小范围。
  • 类型擦除编译后不残留类型信息,运行时不产生额外开销。
  • 逐步收紧从宽松配置起步,逐项打开严格检查,避免一次性淹没在错误里。
TypeScript 类型系统配图:发光盾牌内嵌相互咬合的类型方块
类型像盾牌,把运行时才暴露的错误挡在编译阶段之外。

接口、类型别名与结构类型

interface 与 type 常被拿来比较:接口可以声明合并、更适合描述对象与类的契约;类型别名能表达联合、交叉、条件类型等更灵活的组合。团队里不必强行统一,只要约定「对外暴露的模型用接口,工具类型用别名」即可。真正需要理解的是结构类型:只要形状匹配就能赋值,名字和继承关系不参与判断。

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

type Result<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

const label = (u: Pick<User, "id" | "name">) => `${u.id}: ${u.name}`;

readonly 与可选标记是低成本高回报的写法:前者阻止误改主键,后者如实表达「这个字段可能没有」,逼调用方处理缺失分支。结构类型也意味着「多余属性检查」只在字面量直接赋值时生效,通过变量中转就能绕开,这点在排查报错时经常遇到。

泛型:把类型的决定权交出去

泛型的本质是「类型参数」,让函数或组件在保持类型关联的前提下复用。写泛型时最好带上约束,否则函数体里只能把参数当成任意类型,无法安全访问属性。约束还能表达类型之间的依赖关系,例如「键必须是对象上真实存在的字段名」。

function groupBy<T, K extends keyof T>(items: T[], key: K) {
  return items.reduce((acc, item) => {
    const group = String(item[key]);
    (acc[group] ||= []).push(item);
    return acc;
  }, {} as Record<string, T[]>);
}

const byTag = groupBy(articles, "tag");

调用时第二个参数被限制为 articles 上真实存在的字段名,写错了立刻报错,同时返回值类型自动推导为按分组键聚合后的结构。这种「一次约束,处处受益」的效果,就是泛型相对 any 的根本差别。

类型收窄与穷尽检查

收窄指编译器根据运行时的判断缩小类型范围:typeof、in、instanceof 以及自定义的类型守卫函数都能触发。配合可辨识联合(带字面量标记字段的联合类型),switch 就能做到分支穷尽,任何遗漏都会被编译器指出来。

收窄手段
  • 字面量标记联合成员带一个固定取值的字段,最容易穷尽检查。
  • 类型守卫函数返回 x is T 的函数,适合复用复杂判断。
  • 断言函数返回 asserts,用于抛出错误的校验函数。

工程化配置:tsconfig 与严格模式

新项目建议直接开启全部严格检查。strict 会连带打开空值检查、隐式任意、属性初始化等一组开关,其中 strictNullChecks 是收益最大的一项——它强制把 null 与 undefined 当成独立类型,从此「可能为空」必须显式处理。

跨文件类型检查还需注意隔离:纯粹的类型引入用 import type,避免它被打包工具误判为运行时依赖;路径别名要在 tsconfig 与打包配置中保持一致,否则编译器通过而构建失败,两地配置漂移是团队项目的高频故障点。

另一个实用配置是 noUncheckedIndexedAccess:开启后通过下标读数组元素会得到「可能是 undefined」,逼你先判空再使用。它会让存量代码出现大量报错,但对新项目而言是非常划算的保险,能挡掉大量越界访问导致的崩溃。