TypeScript:给前端加一层「契约」
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:联合、交叉与「不能合并」
本章:
type和interface一样能描述对象形状,但它还能做 联合(或)、交叉(且)、元组、映射等更灵活的类型运算。记住一点:同名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):用「标签」区分形态
定义:当一种数据有多种形态(比如支付结果可能是"成功"或"失败"),每种形态都带一个同名字段(通常叫 kind、type 或 status),并且这个字段的值互不重叠(比如成功是 '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 自动帮你收窄类型。
七、可选、undefined 与 strictNullChecks
本章:开了
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! 是你对编译器说"我担保这里不是 null 或 undefined"。但如果你担保错了,运行时直接炸,TS 不会帮你兜底。只有在你确实有把握(比如刚做过 if (x !== null) 检查)但又懒得改类型签名时才用。
//错误示例
declare const el: HTMLElement | null;
el!.querySelector('a'); // 如果 el 真是 null,运行时报错:Cannot read property of null
啥时候用
- 确实有更强的不变量保证时 — 比如刚
if判断过、React ref 在 useEffect 里一定有值。- 日常开发 — 优先用
?.、??、if检查,别动不动就!。
八、readonly 与 satisfies(现代 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
啥时候用
- 路由表、常量配置 — 既要检查"有没有写错结构",又要保留字面量给后续类型推导用。
九、索引签名、keyof 与 Record
本章:当你不知道对象具体有哪些键,但知道"键是某种类型、值是某种类型"时,用索引签名或
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 会根据你的if、switch等条件判断,自动把类型缩小。
1 三种常见收窄方式
typeof:判断原始类型(string、number、boolean等)。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 防漏
定义:在 switch 的 default 分支里,把值赋给 never 类型的变量。如果你后来新增了一种形态但忘了加 case,TS 会在这里报错——因为那个新增的形态不是 never,赋值会失败。这是一种防漏机制。
// 在前面可辨识联合的 msg 函数里已经展示了:
default: {
const _: never = e; // 如果 e 还有未处理的形态,这里报错
return _;
}
啥时候用
catch (e: unknown)— 先instanceof Error再细分。switch处理多种形态 — 加never检查防遗漏。
十一、unknown 与 any
本章:
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
}
啥时候用
fetch的res.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 / type | interface 描述对象形状(可合并);type 做联合/交叉/运算(不可合并) |
? / ?? / ?. | ? 可选属性;?? 空值合并(只有 null/undefined 才用默认值);?. 可选链(安全访问) |
readonly / as const / satisfies | readonly 浅只读;as const 锁字面量;satisfies 检查形状但不丢精度 |
A | B + 字面量标签 | 可辨识联合——用标签字段区分多种形态 |
unknown + x is T | 外部数据先当 unknown,用守卫函数逐步收窄 |
<T extends U> | 泛型约束——T 至少要有 U 的能力 |
keyof / Record | keyof 取键名联合;Record 快速建"键→值"映射类型 |
Pick / Omit / Partial / Awaited | 从已有类型派生新类型 |
strict / noUncheckedIndexedAccess | 开严格模式 + 数组下标也检查 undefined |
2 结语
TypeScript 的核心收益,来自把"口头约定"变成"编译期能检查的契约":
- 联合 + 收窄写对了,
switch少漏分支,IDE 还能帮你自动补全; unknown守住了数据入口,JSON.parse才不会把整个项目的类型信任链打断;satisfies用在配置表上,既省了注解又不丢字面量精度;- 泛型把输入输出的类型关系绑死,工具函数一处定义、到处安全复用。
最后记住一句:类型是文档,断言是欠条——欠条越少,项目越健康。
更多推荐
所有评论(0)