TypeScript:给前端加一层「契约」

一、导读

定义:用几句话说明「这东西到底解决啥问题」
代码块使用 typescript 标注;含 // 语法要点//正确示例//错误示例
啥时候用列几条「实际写业务时会在哪碰到」

目标:能说清 类型在哪一阶段存在、tsc 与打包器各管什么、联合类型怎么收窄、unknown 与断言的边界;拿到业务里的泛型工具函数,能判断该加 extends 还是该拆成可辨识联合。

TypeScript 是 JavaScript 的超集——在 JS 基础上多了一层类型注解(告诉编译器"这个变量是字符串")和类型运算(组合、拆分、推导类型)。关键点:编译之后,类型就没了,浏览器里跑的仍是 JavaScript。也就是说,类型信息只在写代码和编译时有用,不会参与运行时的逻辑判断(除非你自己写 typeof 之类的检查)。

一句话类比:TypeScript 的类型就像铅笔打的草稿线——画画时帮你对齐,正式出品(编译)后就擦掉了。


二、先搞清楚:类型在哪一层干活

本章:分清 编辑器tsc打包器 各管什么,后面看报错才不会晕。

1 编译期 vs 运行时:两条平行线

定义:类型错误tsc --noEmit 或者 IDE 里那些红色波浪线 阶段就会暴露出来;但像除零、走错分支、网络请求失败这些逻辑问题,类型系统管不了。另外,as 断言只是骗过编译器,让它觉得"嗯,这个值没问题"——运行时数据该是什么还是什么,断言不会帮你转换数据。滥用 as 等于自己把安全带解开。

// 语法要点:类型信息不会进入编译产物(.js 文件里看不到类型)

//正确示例
function add(a: number, b: number): number {
  return a + b;
}

//错误示例(双重断言骗过检查)
const x = 'not a number' as unknown as number;
void x;
// 编译通过了,但运行时 x 仍然是字符串,参与数学运算会 NaN 或逻辑错误

啥时候用

  • 多人协作、接口常改 — 用类型把"这个字段叫什么、是什么类型"钉在代码里,比写在文档站靠谱,文档容易忘更新,代码不会。

2 tsc 与 Vite / webpack:两条线并行

定义:日常项目里,tsc 往往只做类型检查(配置 noEmit: true),实际把 TS 转成 JS 的工作交给 esbuild / swc / Babel(它们转得更快)。所以会出现一种情况:打包通过了,但 tsc 报类型错误——两条线是独立的,打包成功不等于类型没问题。

// 语法要点:类型检查(tsc)和代码转译(esbuild/swc)是两步独立的事

//正确示例——CI 里单独跑类型检查
// npx tsc --noEmit  ← 只检查类型,不产出文件

//错误示例
// 「打包成功了,类型肯定没问题」—— 不一定,Vite/esbuild 默认不跑完整类型检查

啥时候用

  • CI 流水线 — 单独跑 tsc --noEmit,别把类型问题交给打包器「蒙混过关」。

三、注解与推断:少写废话,别写歧义

本章:TypeScript 很聪明,能自己推断出来的类型你就不用手写;但对外导出的公共接口建议写全,因为一旦你改了实现代码,自动推断出的类型可能悄悄变了——这叫类型漂移,调用方不知不觉就拿到了一个和之前不同的类型,容易出 bug。

1 let / const 与字面量类型

定义:用 const 声明一个原始值时,TS 会把它推断成精确到那个值本身的类型(比如 'dev' 而不是宽泛的 string),这叫字面量类型。用 let 声明时,TS 会放宽到宽泛类型(比如 string),因为 let 意味着"以后可能改"。对象想整个锁成只读字面量,加 as const

类比:const 就像刻印章,刻了 'dev' 就只能是 'dev'let 像白板笔,写了 'dev' 但随时可以擦了写别的,所以 TS 把它当成"可能是任意字符串"。

// 语法要点:const 原始值 → 精确字面量类型;as const → 把对象也锁成字面量

//正确示例
const mode = 'dev' as const;
type Mode = typeof mode; // "dev"(不是 string,就是 "dev" 这一个值)

const cfg = { api: '/v1', retry: 3 } as const;
// cfg.api 类型是字面量 "/v1",不能改成别的字符串

//错误示例
let m: 'dev' | 'prod' = 'dev';
m = 'staging'; // 报错:'staging' 不在 'dev' | 'prod' 里

啥时候用

  • 路由名、权限码、Redux action type — 用字面量联合 + as const,拼错一个字编译器就能帮你抓住。

2 元组(Tuple):固定长度、固定顺序的数组

定义:普通数组 number[] 表示"一堆数字,多少个都行";元组 [string, number] 表示"刚好两个,第一个必须是字符串,第二个必须是数字"。像填表格——几栏、每栏填什么类型,都定死了。加 readonly 表示还不能改。

// 语法要点:[T, U] 是定长定序的,T[] 是随意长度的

//正确示例
type Point = readonly [number, number];
const p: Point = [1, 2]; // 坐标点:必须恰好两个数字

//错误示例
const q: Point = [1, 2, 3]; // 报错:长度不匹配,只要 2 个你给了 3 个

啥时候用

  • 坐标点、键值对 — 长度固定、每个位置类型确定的场景。
  • Object.entries() 返回值[string, T][] 就是元组数组。

四、函数:签名、可选参数与 void

本章:函数怎么标注参数和返回值类型,void 是什么意思。

1 参数与返回值

// 语法要点:(a: T, b?: U) => R  其中 ? 表示调用时可以不传

//正确示例
function clamp(n: number, min = 0, max = 100): number {
  return Math.min(max, Math.max(min, n));
}

//错误示例
function parseTitle(s: string): object {
  return JSON.parse(s);
}
// 返回 object 太宽泛了,调用方不知道里面有什么字段
// 应该定义具体的 interface,或者返回 unknown 再配合类型守卫

2 void:表示"我不关心返回值"

定义:void 用在回调函数类型里,表示"调用这个回调的人不需要你返回什么"。有个容易踩的坑:即使回调类型写的是 () => void,你在实现里 return 123 也不报错(这是 TS 的历史设计),但调用方拿不到这个返回值——别依赖这个行为。

// 语法要点:回调写成 () => void,表示"用完就扔,不看你返回啥"

//正确示例
const nums = [1, 2, 3];
nums.forEach((n) => {
  console.log(n);
  return n * 2; // 不报错,但 forEach 根本不看返回值,写了也白写
});

啥时候用

  • Array.prototype.forEach、事件回调 — 常见 void 返回类型。

五、interface:给对象画一张「蓝图」

本章interface 用来描述"一个对象应该长什么样"——有哪些字段、每个字段什么类型。它有个特殊能力:同名 interface 会自动合并(声明合并),适合给第三方类型"打补丁";但业务代码里要避免多人写同名 interface,否则合并出冲突类型变 never

1 基本形状与继承 extends

// 语法要点:interface A extends B { ... }  A 继承 B 的所有字段,再补充自己的

interface Timestamps {
  createdAt: string;
}

interface User extends Timestamps {
  id: string;
  name: string;
  email?: string; // ? 表示可选,调用时可以不传
}

2 声明合并(慎用!)

定义:两个同名的 interface 会自动把字段合并到一起。这在给全局类型"补丁"时有用(比如给 Window 加自定义属性),但在同一个业务实体上这么做很危险——两个文件各写一个 interface User,如果同一个字段类型不一致,合并后就变成 never(永远不可能有值的类型),直接炸。

//正确示例——在 .d.ts 文件里给全局 Window 补一个自定义属性
// declare global { interface Window { __APP__: { version: string } } }

//错误示例——同一业务实体被拆成两次同名 interface
interface User {
  id: string;
}
interface User {
  id: number;
}
// id 变成 string & number → never(不可能同时是 string 又是 number)

啥时候用

  • 组件 props、DTO — 一个文件里一个 interface,管一个概念。
  • 声明合并 — 只用于给全局/第三方类型"打补丁",别用在业务实体上。

六、type:联合、交叉与「不能合并」

本章typeinterface 一样能描述对象形状,但它还能做 联合(或)、交叉(且)、元组、映射等更灵活的类型运算。记住一点:同名 type 重复声明会直接报错(不像 interface 会合并),所以 type 更安全。

1 联合 |(或)与交叉 &(且)

定义:

  • 联合 |:类型 A 或者 类型 B,满足其中一个就行。比如 string | number 表示"字符串或数字都行"。
  • 交叉 &:类型 A 并且 类型 B,必须同时满足。常用在"基于某个类型扩展几个字段"的场景。

交叉用过头会得到 never——比如 { a: 1 } & { a: 2 } 意思是"a 既是 1 又是 2",不可能有这种值。

// 语法要点:A | B = "A 或 B";A & B = "同时是 A 和 B"

type Id = string | number; // ID 可以是字符串或数字

type Admin = User & { role: 'admin' }; // 在 User 基础上,多一个 role 字段

//错误示例
type Nope = { a: 1 } & { a: 2 }; // a 既要等于 1 又要等于 2 → never,不可能有合法值

2 可辨识联合(discriminated union):用「标签」区分形态

定义:当一种数据有多种形态(比如支付结果可能是"成功"或"失败"),每种形态都带一个同名字段(通常叫 kindtypestatus),并且这个字段的值互不重叠(比如成功是 'success',失败是 'fail')。这样 TS 就能根据这个标签字段的值,自动推断出"哦,这是成功的那一种形态",就能安全地访问该形态特有的字段——这个自动推断过程叫收窄

类比:可辨识联合就像快递包裹上的标签贴纸——看到贴了"易碎"就知道要轻拿,贴了"加急"就知道要快送。TS 看到标签字段的值,就知道该按哪种形态处理。

// 语法要点:每个形态都有同名字段(标签),标签的值用字面量(不能是宽泛 string)

type PayEvent =
  | { kind: 'success'; orderId: string }
  | { kind: 'fail'; orderId: string; reason: string };

function msg(e: PayEvent): string {
  switch (e.kind) {
    case 'success':
      // TS 在这个分支里知道 e.kind 是 'success',所以 e 的类型就是第一种形态
      return `订单 ${e.orderId} 成功`;
    case 'fail':
      // TS 在这个分支里知道 e.kind 是 'fail',所以能安全访问 e.reason
      return `订单 ${e.orderId} 失败:${e.reason}`;
    default: {
      // never 检查:如果将来加了新的形态但忘了加 case,这里会报错提醒你
      const _: never = e;
      return _;
    }
  }
}
//错误示例——标签字段写成宽泛的 string,TS 无法区分到底是哪种形态
type Loose = { tag: string; n: number } | { tag: string; s: string };
function bad(x: Loose) {
  if (x.tag === 'a') {
    return x.n; // 报错:TS 不知道是第一种形态,因为 tag 是 string,两种形态都匹配
  }
}

啥时候用

  • 接口返回多种形态、状态机 — 用字面量标签区分,TS 自动帮你收窄类型。

七、可选、undefinedstrictNullChecks

本章:开了 strictNullChecks(包含在 strict 里)之后,可选属性读取时可能是 undefined,必须处理"值不存在"的情况——否则编译器会拦你。

// 语法要点:?.(可选链)、??(空值合并)配合使用

interface Config {
  timeout?: number; // 可选,可能有值也可能是 undefined
}

function readTimeout(c: Config): number {
  return c.timeout ?? 5000; // timeout 不存在时用 5000
}

//错误示例
function bad(c: Config): number {
  return c.timeout; // 报错:timeout 可能是 undefined,不能直接当 number 返回
}

1 非空断言 !(尽量少用)

定义:x! 是你对编译器说"我担保这里不是 nullundefined"。但如果你担保错了,运行时直接炸,TS 不会帮你兜底。只有在你确实有把握(比如刚做过 if (x !== null) 检查)但又懒得改类型签名时才用。

//错误示例
declare const el: HTMLElement | null;
el!.querySelector('a'); // 如果 el 真是 null,运行时报错:Cannot read property of null

啥时候用

  • 确实有更强的不变量保证时 — 比如刚 if 判断过、React ref 在 useEffect 里一定有值。
  • 日常开发 — 优先用 ?.??if 检查,别动不动就 !

八、readonlysatisfies(现代 TS 必备)

本章readonly 标记"这个属性不能重新赋值",但只管一层(浅只读);satisfies(TS 4.9+)让你既保留精确的字面量类型,又检查是否符合某个大形状。

1 readonly 是浅的

定义:readonly 只锁住当前层级的赋值。对象里面嵌套的对象,该改还能改。

interface Box {
  readonly inner: { count: number };
}
const b: Box = { inner: { count: 0 } };
b.inner = { count: 1 }; // 报错:inner 本身是 readonly
b.inner.count++; // 合法!readonly 只管 inner 这一层,不管 inner 里面的 count

2 satisfies:检查形状但不丢失精度

定义:const x = { ... } satisfies SomeType 的意思是:先按你写的字面量推断精确类型,然后检查这个值是否满足 SomeType 的形状。和 as 不同——as 是"我声明它就是这个类型"(断言,可能是骗编译器),satisfies 是"请帮我检查它是否符合,但保留原始的精确类型信息"。

// 语法要点:值保留字面量精度 + 结构校验

type Routes = Record<string, { title: string }>;

const routes = {
  '/': { title: '首页' },
  '/about': { title: '关于' },
} satisfies Routes;
// 通过了 Routes 的结构检查,同时 routes['/'] 仍然是精确的 { title: "首页" }
// 如果写成 const routes: Routes = {...},title 就变成宽泛的 string 了

//error示例
const bad = {
  '/': { title: 1 },
} satisfies Routes; // 报错:title 必须是 string,你给了 number

啥时候用

  • 路由表、常量配置 — 既要检查"有没有写错结构",又要保留字面量给后续类型推导用。

九、索引签名、keyofRecord

本章:当你不知道对象具体有哪些键,但知道"键是某种类型、值是某种类型"时,用索引签名或 Record

定义:

  • 索引签名 [k: string]: T 表示"任意字符串键,值都是 T"。
  • keyof T 得到某个类型所有键名的联合(比如 "id" | "name")。
  • Record<K, V> 是索引签名的简写,等价于 { [P in K]: V }
// 语法要点:Record<K,V> 是 { [P in K]: V } 的简写

type Role = 'admin' | 'user';

const perms: Record<Role, string[]> = {
  admin: ['*'],
  user: ['read'],
};

type Keys = keyof typeof perms; // "admin" | "user"
//错误示例
interface Bad {
  [key: string]: string; // 所有值都必须是 string
  id: number; // 报错:number 不能赋给索引值类型 string
}
// 规则:明确写出的字段类型,必须能赋给索引签名的值类型

啥时候用

  • 固定几个键的映射Record + satisfies 双保险,既有类型检查又能拼错键时报错。

十、收窄:让 TS 从"宽泛"变成"精确"

本章:"收窄"就是从一个宽泛的类型(比如 string | number)缩小到一个更精确的类型(比如确定它是 string。TS 会根据你的 ifswitch 等条件判断,自动把类型缩小。

1 三种常见收窄方式

  • typeof:判断原始类型(stringnumberboolean 等)。
  • in:判断对象有没有某个属性(适合区分不同的对象形态)。
  • instanceof:判断是不是某个 class 的实例。
// typeof 收窄
function fmt(x: string | number) {
  if (typeof x === 'string') {
    // 这里 TS 知道 x 是 string,可以调用字符串方法
    return x.toUpperCase();
  }
  // 这里 TS 知道 x 一定是 number
  return x.toFixed(2);
}

// instanceof 收窄
class ApiError extends Error {
  code = 500 as const;
}
function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError;
  // 返回 true 时,TS 会把 e 的类型从 unknown 收窄为 ApiError
  // 这种写法叫"自定义类型守卫"——就是一个返回布尔值的函数,顺便告诉 TS 类型信息
}

2 穷尽检查:用 never 防漏

定义:在 switchdefault 分支里,把值赋给 never 类型的变量。如果你后来新增了一种形态但忘了加 case,TS 会在这里报错——因为那个新增的形态不是 never,赋值会失败。这是一种防漏机制

// 在前面可辨识联合的 msg 函数里已经展示了:
default: {
  const _: never = e; // 如果 e 还有未处理的形态,这里报错
  return _;
}

啥时候用

  • catch (e: unknown) — 先 instanceof Error 再细分。
  • switch 处理多种形态 — 加 never 检查防遗漏。

十一、unknownany

本章any 是"关闭类型检查"(什么都能做,但不安全);unknown 是"我不知道是什么类型,但你必须先检查再使用"(安全但需要多写几行)。

1 承接 JSON:从 any 换成 unknown

定义:JSON.parse 标准返回类型是 any——意味着你可以随便用,TS 不会拦你,但如果 JSON 结构和你想的不一样,运行时就炸。更安全的做法是封装一层返回 unknown,然后用类型守卫函数(就是上面提到的 e is ApiError 那种)逐步检查。

interface User {
  id: string;
  name: string;
}

// 封装:返回 unknown,强制调用方先检查
function parseUnknown(raw: string): unknown {
  return JSON.parse(raw);
}

// 类型守卫:检查 unknown 值是否是 User
function isUser(v: unknown): v is User {
  if (typeof v !== 'object' || v === null) return false;
  const o = v as Record<string, unknown>;
  return typeof o.id === 'string' && typeof o.name === 'string';
}

function loadUser(raw: string): User {
  const v = parseUnknown(raw);
  if (isUser(v)) return v; // isUser 返回 true 后,TS 把 v 收窄为 User
  throw new Error('Invalid user JSON');
}

//错误示例
function bad(raw: string): User {
  return JSON.parse(raw); // any 直通,完全没有保护——JSON 里可能根本没有 id 和 name
}

啥时候用

  • fetchres.json()postMessage、URL 参数 — 一律先 unknown 再守卫检查。

2 any 的逃生舱与 @ts-expect-error

定义:实在搞不定类型时,@ts-expect-error 可以临时压制下一行的类型错误。它比 @ts-ignore 好:如果下一行其实没有类型错误@ts-expect-error 本身会报错(防止压制指令遗留)。

//正确示例(下一行确实有类型错误;修好业务代码后记得删掉这行注释)
// @ts-expect-error 演示:number 不能赋给 string
const _demo: string = 1;

//错误示例(下一行没有错误,指令本身反而报错)
// @ts-expect-error
const ok = 1; // 报错:Unused '@ts-expect-error' directive

啥时候用

  • 临时压制 — 修好代码后立即删除,别留一堆 @ts-expect-error 在仓库里。

十二、泛型:把「类型」当参数传

本章:泛型 <T> 的作用是把"输入和输出的类型关系"绑在一起。比如"传入 number 数组,返回 number 或 undefined"——这个 number 不是写死的,而是由调用时传入的数组元素类型决定的。

1 基本用法:推断与约束

// 语法要点:function f<T>(x: T)  其中 T 由调用时自动推断

// T 自动推断为 number(因为传入的是 [1, 2, 3])
function first<T>(arr: readonly T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]); // n 的类型是 number | undefined

// 约束:T 必须有 id 字段
function pickId<T extends { id: string }>(x: T) {
  return x.id; // 安全:TS 保证 T 一定有 id
}
//错误示例
function bad<T>(x: T) {
  return x.id; // 报错:T 可能是任何类型,不一定有 id
}
// 修复:加上 extends 约束,如上面的 pickId

2 泛型与 async

// 泛型 + unknown 守卫 + async 的典型组合
async function loadMany<T>(urls: readonly string[], parse: (j: unknown) => T): Promise<T[]> {
  const out: T[] = [];
  for (const url of urls) {
    const res = await fetch(url);
    if (!res.ok) throw new Error(String(res.status));
    out.push(parse(await res.json())); // parse 函数负责把 unknown 变成 T
  }
  return out;
}

啥时候用

  • 列表请求 + 统一解析 — 泛型 + unknown 守卫,一套代码适配多种数据类型。
  • 工具函数 — 入参和返回值类型有关联关系的场景。

十三、与 fetch / JSON:类型不能代替运行时校验

本章getJson<User>(url) 里的 User 只在写代码时帮你补全和检查——运行时后端可能返回任何东西(改了字段、返回 HTML 错误页、网络被劫持)。所以在和外部系统对接时,运行时也要用 schema 校验(如 Zod)或手写守卫

async function getJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data: unknown = await res.json();
  return data as T; // 这里 as T 是"我相信后端返回的就是 T"——信任但不验证
}

//错误认知
// 「只要写了 getJson<User>,线上就一定拿到 User」
// —— 后端改了字段名、返回了 HTML 错误页、网络被篡改,运行时仍然会出问题

啥时候用

  • 内网强契约 — 类型 + 契约测试 + 必要时 schema 校验,三道防线。
  • 对外部 API — 一定要有运行时校验,类型只是第一道防线。

十四、工具类型:TS 自带的「类型变形器」

本章:TS 内置了一堆工具类型,帮你从现有类型"变形"出新类型——挑几个字段、删几个字段、全变成可选、全变成必选等。

1 常用工具类型一览

工具类型干什么例子
Partial<T>T 的所有字段变可选改部分字段时用
Required<T>T 的所有字段变必选和 Partial 反过来
Pick<T, K>从 T 中挑几个字段只暴露部分字段
Omit<T, K>从 T 中删几个字段隐藏内部字段
Readonly<T>T 的所有字段变只读防止误修改
Record<K, V>键为 K、值为 V 的对象固定键集合的映射
ReturnType<F>函数 F 的返回值类型不用重复写返回类型
Parameters<F>函数 F 的参数类型元组提取参数类型
Awaited<T>Promise<T> 拆成 T异步函数返回值解包
interface Todo {
  id: string;
  title: string;
  done: boolean;
}

// PATCH 请求:只改部分字段
type Patch = Partial<Pick<Todo, 'title' | 'done'>>;

// 对外暴露:去掉 id
type PublicTodo = Omit<Todo, 'id'>;

async function createTodo(body: PublicTodo) {
  return body;
}

// 拿到异步函数的返回值类型(自动解开 Promise)
type CreateReturn = Awaited<ReturnType<typeof createTodo>>;
//注意:Omit<Todo, 'ghost'> 不报错——不存在的键被静默忽略,别拿它当拼写检查用

啥时候用

  • DTO 转换 — 入参/出参和数据库实体不一样时,用 Pick/Omit 派生。
  • 表单编辑Partial 让所有字段可选(编辑时可能只改一部分)。

十五、tsconfig:从 strict 到模块解析

本章tsconfig.json 是 TS 项目的配置文件。新项目建议直接开 strict: true(最严格模式),少踩坑。

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": false,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "verbatimModuleSyntax": false,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src"]
}

定义(几个关键配置项):

  • strict:打开所有严格检查的集合开关。
  • noUncheckedIndexedAccess:数组下标访问(arr[0])和 Record 键访问时,返回值自动多一个 | undefined,更贴近运行时真实情况。
  • moduleResolution: "Bundler":配合 Vite 等打包器使用;纯 Node 库常用 "NodeNext"
  • verbatimModuleSyntax: true:要求严格区分 import type(只导入类型)和值 import(运行时真正导入)。和旧代码混用时容易报错,建议团队统一规范后再开。
  • noEmit: true:只做类型检查,不输出 JS 文件(转译交给打包器)。

啥时候用

  • 新项目 — 直接开 strict + noUncheckedIndexedAccess
  • 旧项目迁移 — 可以逐步开,不用一步到位。

十六、enum 还是字面量联合?

本章:TS 的 enum(枚举)会生成额外的 JS 代码(数字枚举还有反向映射对象),增加打包体积。多数前端场景下,字面量联合 + as const 更轻量、更透明。

// 数字枚举(编译后会生成一个 JS 对象)
enum Status {
  Idle,
  Busy,
}

//字面量联合(零额外 JS 代码,类型信息编译后擦除)
type Phase = 'idle' | 'busy' | 'done';
const PHASE = ['idle', 'busy', 'done'] as const;
type PhaseFromTuple = (typeof PHASE)[number]; // "idle" | "busy" | "done"

啥时候用

  • 新代码 — 默认用字面量联合。
  • 与 Java 等后端互操作需要数字枚举 — 再考虑 enum

十七、常见误区(再纠一遍)

误区事实
「上了 TS 就不会线上炸」类型检查 + 单元测试 + 运行时校验 缺一不可,类型只管编译期
as 能帮我转换类型」断言不转换数据,只是骗编译器"别管了";as unknown as Foo 是双重欺骗
interface 合并很强大」业务模型忌同名多文件合并,同字段类型不一致就变 never
「泛型嵌套十层很酷」推断失败时错误信息根本看不懂,适度拆分才是好设计
enum 比联合高级」多数前端场景 字面量联合更轻、更透明、更好 tree-shaking

十八、速查总表与结语

1 速查表

语法 / 概念一句话
: T类型注解——告诉 TS 这个值是什么类型
interface / typeinterface 描述对象形状(可合并);type 做联合/交叉/运算(不可合并)
? / ?? / ?.? 可选属性;?? 空值合并(只有 null/undefined 才用默认值);?. 可选链(安全访问)
readonly / as const / satisfiesreadonly 浅只读;as const 锁字面量;satisfies 检查形状但不丢精度
A | B + 字面量标签可辨识联合——用标签字段区分多种形态
unknown + x is T外部数据先当 unknown,用守卫函数逐步收窄
<T extends U>泛型约束——T 至少要有 U 的能力
keyof / Recordkeyof 取键名联合;Record 快速建"键→值"映射类型
Pick / Omit / Partial / Awaited从已有类型派生新类型
strict / noUncheckedIndexedAccess开严格模式 + 数组下标也检查 undefined

2 结语

TypeScript 的核心收益,来自把"口头约定"变成"编译期能检查的契约"

  • 联合 + 收窄写对了,switch 少漏分支,IDE 还能帮你自动补全;
  • unknown 守住了数据入口,JSON.parse 才不会把整个项目的类型信任链打断;
  • satisfies 用在配置表上,既省了注解又不丢字面量精度;
  • 泛型把输入输出的类型关系绑死,工具函数一处定义、到处安全复用。

最后记住一句:类型是文档,断言是欠条——欠条越少,项目越健康。

更多推荐