关于 typescript-advanced-patterns
面向 TypeScript 开发者的高级类型系统参考技能,通过编译期约束减少运行时错误。内容涉及品牌类型、联合类型、模式推断等,也说明何时应优先采用简单类型。
主要功能
- 01品牌类型与名义类型
- 02可辨识联合与穷尽检查
- 03模板字面量类型与条件类型
- 04Zod 模式推断
- 05类型安全的事件发射器模式
适用场景
- 01防止混用不同标识符
- 02建模 API 结果与状态变体
- 03从经验证的外部数据推导类型
- 04构建具备类型化事件的发射器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill typescript-advanced-patterns安装后在 Claude Code 中输入/typescript-advanced-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/branded-types.mdtype-safe-patterns.md详细介绍
TypeScript 高级模式
通过在编译时编码约束,消除运行时错误的高级类型系统模式。
适用场景
适用于:“品牌类型”、“名义类型”、“可辨识联合”、“模板字面量类型”、“条件类型”、“infer 关键字”、“satisfies 运算符”、“const 断言”、“Zod 推断”、“穷尽式 switch”、“类型安全的事件发射器”、“映射类型”、“工具类型”、“泛型约束”、“类型收窄”、“as const”
**不适用于:**TypeScript 基础语法 | React 组件属性类型 | 通用 JavaScript 模式
决策树:针对你的问题选择哪种高级模式?
flowchart TD
P[What is your problem?] --> P1[I'm mixing up\nvalues of the same type\ne.g. UserId vs OrderId]
P[What is your problem?] --> P2[I have a value that\ncan be one of N shapes]
P[What is your problem?] --> P3[I need type-safe\nevent emitting]
P[What is your problem?] --> P4[I'm parsing external\ndata and want types]
P[What is your problem?] --> P5[I want types that\ndepend on other types]
P[What is your problem?] --> P6[My switch/if is\nnot exhaustive]
P1 --> S1[Branded Types\nfor nominal typing]
P2 --> S2[Discriminated Unions\nwith type narrowing]
P3 --> S3[Type-Safe Event Emitter\nwith mapped types]
P4 --> S4[Zod schema + z.infer\nfor runtime + compile time]
P5 --> S5[Conditional Types\nwith infer keyword]
P6 --> S6[Exhaustive checking\nwith never type]
核心模式
1. 品牌类型(名义类型)
TypeScript 的结构类型系统意味着 UserId 和 string 可以相互赋值。品牌类型会添加一个虚构品牌,使两者无法兼容。
// Without branding: these compile without error
function chargeUser(userId: string, amount: number) { /* ... */ }
const orderId = getOrderId();
chargeUser(orderId, 100); // Wrong! orderId passed as userId — TypeScript allows it
// With branding: compile-time protection
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type EmailAddress = Brand<string, 'EmailAddress'>;
type Cents = Brand<number, 'Cents'>; // Never pass raw numbers as money
// Constructor functions (only way to create branded values)
const UserId = (id: string): UserId => id as UserId;
const OrderId = (id: string): OrderId => id as OrderId;
const Cents = (n: number): Cents => {
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid cents: ${n}`);
return n as Cents;
};
// Usage: type error if you mix them up
function chargeUser(userId: UserId, amount: Cents) { /* ... */ }
const uid = UserId('user_123');
const oid = OrderId('order_456');
chargeUser(uid, Cents(1000)); // OK
chargeUser(oid, Cents(1000)); // Error: Argument of type 'OrderId' is not assignable to 'UserId'
chargeUser(uid, 1000); // Error: Argument of type 'number' is not assignable to 'Cents'
有关 Zod 集成和数据库模型模式,请参阅 references/branded-types.md。
2. 使用类型收窄的可辨识联合
kind(或 type、tag)字段是判别字段。检查该字段时,TypeScript 会收窄联合类型。
type ApiResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; code: number; message: string }
| { status: 'loading' };
function handleResult<T>(result: ApiResult<T>): T | null {
switch (result.status) {
case 'success': return result.data; // TypeScript knows data exists here
case 'error': console.error(result.code, result.message); return null;
case 'loading': return null;
}
}
使用穷尽检查时——如果添加了新变体,但没有更新 switch,就会出现编译错误:
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}
function handleResult<T>(result: ApiResult<T>): T | null {
switch (result.status) {
case 'success': return result.data;
case 'error': return null;
// If 'loading' is not handled, TypeScript errors:
// Argument of type '{ status: "loading" }' is not assignable to 'never'
default: return assertNever(result);
}
}
3. 模板字面量类型
在类型层面组合字符串字面量,为基于字符串的 API 创建精确类型:
// Route path types — prevents typos in route definitions
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'orders' | 'products';
type ApiEndpoint = `/${ApiVersion}/${Resource}`;
// Type: "/v1/users" | "/v1/orders" | "/v1/products" | "/v2/users" | ...
// CSS-in-JS property types
type CSSProperty = 'margin' | 'padding' | 'border';
type Side = 'top' | 'right' | 'bottom' | 'left';
type CSSPropertyWithSide = `${CSSProperty}-${Side}`;
// Type: "margin-top" | "margin-right" | ... | "border-left"
// Event name types
type DOMEventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
input: InputEvent;
};
type EventName = `on${Capitalize<keyof DOMEventMap>}`;
// Type: "onClick" | "onKeydown" | "onInput"
4. 使用 infer 的条件类型
infer 会在条件类型求值期间从另一种类型中提取类型。
// Extract the return type of an async function
type Awaited<T> = T extends Promise<infer U> ? U : T;
type UserFetch = () => Promise<{ id: string; name: string }>;
type User = Awaited<ReturnType<UserFetch>>;
// Type: { id: string; name: string }
// Extract the element type of an array
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Names = ElementOf<string[]>; // string
type Events = ElementOf<Array<{ id: number; type: string }>>; // { id: number; type: string }
// Extract handler parameters from an event map
type HandlerParams<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// Deeply unwrap Promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Result = DeepAwaited<Promise<Promise<string>>>; // string
5. satisfies 运算符(TypeScript 4.9+)
satisfies 会根据某种类型验证值,而不会扩大其类型。你既能获得类型检查,也能保留最具体的推断类型。
type Color = string | [number, number, number];
// Problem with 'as': loses specific type info
const colors = {
red: [255, 0, 0] as Color, // type is Color, not [255, 0, 0]
blue: [0, 0, 255] as Color,
} as Record<string, Color>;
// Problem with annotation: same widening
const colors2: Record<string, Color> = {
red: [255, 0, 0], // type is Color
};
// satisfies: validates AND preserves specific type
const colors3 = {
red: [255, 0, 0],
blue: [0, 0, 255],
} satisfies Record<string, Color>;
colors3.red.map(x => x * 2); // OK: TypeScript knows it's [number, number, number]
colors3.blue[0]; // OK: indexed access works
// Great for config objects
const config = {
port: 3000,
host: 'localhost',
debug: false,
} satisfies {
port: number;
host: string;
debug: boolean;
timeout?: number; // optional fields allowed to be absent
};
config.port.toFixed(0); // OK: port is still `number`, not widened to `number | string`
6. Zod 模式推断
Zod 可在运行时进行验证,并自动提供 TypeScript 类型。不要分别编写类型和验证器。
import { z } from 'zod';
// Define schema once
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'moderator']),
createdAt: z.coerce.date(),
metadata: z.record(z.string(), z.unknown()).optional(),
});
// Extract type — no duplicate type definition
type User = z.infer<typeof UserSchema>;
// Type: { id: string; email: string; role: "admin" | "user" | "moderator"; createdAt: Date; metadata?: Record<string, unknown> }
// Parse with validation
function parseUser(raw: unknown): User {
return UserSchema.parse(raw); // throws ZodError with structured errors on failure
}
// Safe parse (returns Result type)
const result = UserSchema.safeParse(rawData);
if (result.success) {
result.data.role; // narrowed to User
} else {
result.error.issues; // structured validation errors
}
// Derive related schemas
const CreateUserInput = UserSchema.omit({ id: true, createdAt: true });
type CreateUserInput = z.infer<typeof CreateUserInput>;
const UpdateUserInput = UserSchema.partial().required({ id: true });
有关类型安全的事件发射器、构建器模式和穷尽检查工具,请参阅 references/type-safe-patterns.md。
参考文件
| 文件 | 内容 |
|---|---|
references/branded-types.md | 用于 ID/货币/电子邮件的品牌基本类型、Zod 集成、数据库模式 |
references/type-safe-patterns.md | 穷尽式 switch、构建器模式、类型安全的事件发射器、映射类型工具 |
反模式(识别标志)
反模式 1:用 any 逃避问题,而不是使用恰当的泛型
新手想法:“这个类型太复杂了,我就用 any,以后再处理。”
为什么不对:any 会禁用对该值的所有类型检查,并传播到它接触的所有内容。any 具有传染性——一个值一旦变成 any,接收它的函数就会将返回类型推断为 any,从而形成一个会随时间扩大的类型漏洞。“以后再处理”最终永远不会发生。
识别方法:代码库中的 as any 几乎总是意味着类型设计有问题,而非 TypeScript 有局限。
修复——使用带约束的泛型:
// Bad: uses any to avoid figuring out the right type
function processItems(items: any[]): any[] {
return items.filter(item => item.active);
}
// Good: generic with constraint
function processItems<T extends { active: boolean }>(items: T[]): T[] {
return items.filter(item => item.active);
}
// Better: if you just need "has a property", use unknown and narrow
function getProperty(obj: unknown, key: string): unknown {
if (typeof obj === 'object' && obj !== null && key in obj) {
return (obj as Record<string, unknown>)[key];
}
return undefined;
}
修复——对真正未知的数据使用 unknown,而不是 any:
// Bad: any propagates
function parseData(raw: any) {
return raw.user.id; // No error, will crash if shape is wrong
}
// Good: unknown forces you to narrow
function parseData(raw: unknown) {
if (
typeof raw === 'object' &&
raw !== null &&
'user' in raw &&
typeof (raw as any).user === 'object'
) {
// Now you can access safely
}
// Better: use Zod
return UserSchema.parse(raw);
}
识别标志:any 表示“我不知道,而且我不想让 TypeScript 检查。”unknown 表示“我不知道,但使用前我会先检查。”在 API 边界优先使用 unknown,绝不使用 any。
反模式 2:简单类型足够时却过度设计类型
新手想法(TypeScript 进阶用户):“我可以用条件类型和模板字面量,把整套业务规则都编码到类型系统里!”
为什么不对:类型复杂度确实会带来成本。带有多个 infer 层级的条件类型会生成错误消息,需要花三分钟才能看懂。初级工程师无法维护这些类型。IDE 自动补全会慢得令人难以忍受。计算耗时 200ms 的类型会让整个文件的编辑体验变得很糟。
校准问题:这种类型复杂度是否能避免一类确实可能发生的错误?如果能,就值得。如果你只是在证明自己做得到,那就简化。
合适与过度设计的示例:
// APPROPRIATE: Branded types prevent real mixing bugs
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
// APPROPRIATE: Discriminated union encodes real state machine
type AuthState =
| { status: 'logged_out' }
| { status: 'logging_in' }
| { status: 'logged_in'; user: User; token: string };
// OVER-ENGINEERED: Encoding HTTP method semantics in types
// Does this actually prevent bugs? Probably not.
type HttpMethodHasBody<M extends HttpMethod> =
M extends 'POST' | 'PUT' | 'PATCH' ? true : false;
type RequestWithBody<M extends HttpMethod> =
HttpMethodHasBody<M> extends true
? { method: M; body: unknown }
: { method: M; body?: never };
// OVER-ENGINEERED: Recursive type for deeply nested access
// Causes slow compilation and unreadable errors
type DeepGet<T, Path extends string> =
Path extends `${infer Head}.${infer Tail}`
? Head extends keyof T
? DeepGet<T[Head], Tail>
: never
: Path extends keyof T
? T[Path]
: never;
// Use lodash.get + unknown return type instead
决策经验法则:如果你无法用一句话解释这个类型,就需要添加注释。如果需要超过两句话,请重新考虑更简单的方案(运行时验证、辅助函数)是否更易于维护。
识别标志:精通 TypeScript 的工程师知道何时不该使用高级类型。目标是减少错误并改善 DX,而不是展示对类型系统的掌握。当简单联合类型和复杂条件类型都能防止相同的错误时,应优先选择简单联合类型。
质量检查清单
[ ] No bare `any` types — unknown or generics used instead
[ ] Primitive values that must not be mixed are branded (IDs, money, emails)
[ ] Sum types use discriminated unions, not boolean flags
[ ] External data parsed through Zod schemas (never typed as a known type without validation)
[ ] Switch statements over union types use exhaustive checking
[ ] Conditional types include a comment explaining what they compute
[ ] No type aliases that just rename primitives without branding
[ ] Generic constraints are as specific as needed, no more
[ ] satisfies used for config objects instead of widening assertions
[ ] Type-level tests (expect-type) for complex utility types
输出产物
- 领域类型模块 — 为所有基础类型的领域值(ID、金额、电子邮件)定义品牌类型
- Zod 模式 — 定义模式并导出
z.infer类型 - 可辨识联合定义 — 状态机、API 结果、领域事件
- 类型安全的事件发射器 — 使用带类型事件映射的泛型 EventEmitter
- 实用类型 — 项目可复用的条件类型工具