mcpField
T
typescript-advanced-patterns

typescript-advanced-patterns

github.com/curiositech开发263140 次安装2 天前更新
代码仓库

关于 typescript-advanced-patterns

面向 TypeScript 开发者的高级类型系统参考技能,通过编译期约束减少运行时错误。内容涉及品牌类型、联合类型、模式推断等,也说明何时应优先采用简单类型。

主要功能

  1. 01品牌类型与名义类型
  2. 02可辨识联合与穷尽检查
  3. 03模板字面量类型与条件类型
  4. 04Zod 模式推断
  5. 05类型安全的事件发射器模式

适用场景

  1. 01防止混用不同标识符
  2. 02建模 API 结果与状态变体
  3. 03从经验证的外部数据推导类型
  4. 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.json
references/
branded-types.md
type-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

输出产物

  1. 领域类型模块 — 为所有基础类型的领域值(ID、金额、电子邮件)定义品牌类型
  2. Zod 模式 — 定义模式并导出 z.infer 类型
  3. 可辨识联合定义 — 状态机、API 结果、领域事件
  4. 类型安全的事件发射器 — 使用带类型事件映射的泛型 EventEmitter
  5. 实用类型 — 项目可复用的条件类型工具

评论

支持 Markdown