mcpField
T
typescript-discriminated-unions

typescript-discriminated-unions

github.com/mkosir开发7854.0k 次安装8 周前更新
代码仓库

关于 typescript-discriminated-unions

为 TypeScript 项目提供判别联合类型的编码指导,面向实现或审查类型模型的开发者。强调先遵循仓库约定,并根据变体是否互斥选择合适的数据结构。

主要功能

  1. 01按任务应用类型指导
  2. 02优先遵循仓库约定
  3. 03为互斥变体使用判别联合
  4. 04区分可选属性与变体属性
  5. 05考虑状态及函数参数建模

适用场景

  1. 01建模具有不同数据的互斥状态
  2. 02设计带不同属性的函数参数
  3. 03为 React 组件定义变体属性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mkosir/typescript-style-guide --skill typescript-discriminated-unions

安装后在 Claude Code 中输入/typescript-discriminated-unions确认已加载。

详细介绍

可辨识联合

在当前任务中,按照 TypeScript 风格指南的可辨识联合约定进行处理。

工作流程

  1. 检查使用方仓库的约定和配置。
  2. 明确的仓库约定优先于这份主观指导。
  3. 仅应用、审查或说明与任务相关的指导。
  4. 如果合适的模型取决于上下文或判断,请说明重要的权衡。

边界

  • 对需要不同数据的互斥变体使用可辨识联合。
  • 如果属性可能独立缺失,或只有值会变化,不要强行使用可辨识联合。
  • 将 TypeScript 和 ESLint 能自动执行的检查交由它们负责。
  • 不要仅仅因为启用了此技能,就引入无关的 TypeScript 风格指南约定。

可辨识联合 {#discriminated-unions}

如果只能选择一项 TypeScript 特性,就选择可辨识联合。

可辨识联合是共享一个属性且该属性具有不同字面量值的对象类型联合。检查该属性时,类型会收窄为匹配的变体。

当变体互斥且每个变体都需要不同的数据时,请使用可辨识联合。如果属性可能独立缺失,请将其保留为可选属性;如果只有值会变化,则使用字面量联合。

当变体表示命名状态或模式,且由你控制其结构时,优先使用共享的字面量辨识属性。只有当属性是否存在本身就是自然的区分方式,且添加辨识属性会降低 API 的清晰度时,才使用可选的 never 属性。

可辨识联合是一种强大的概念,可用于建模复杂的数据结构并提升类型安全性,从而让代码更清晰、更不易出错。
你可能会遇到可辨识联合的其他称呼,例如标记联合或和类型;在 C、Haskell 和 Rust 等语言中(结合模式匹配)也有类似概念。

可辨识联合的优点:

  • 如 必需和可选对象属性、函数参数 和 将属性定义为可辨识类型 中所述,可辨识联合会将依赖变体的可选属性替换为该变体必需的属性,从而降低复杂度。

  • 穷尽性检查——已配置的 ESLint 规则会在 switch 未处理可辨识联合的所有变体时报告问题。

    {"@typescript-eslint/switch-exhaustiveness-check": "error"}

    type Circle = { kind: 'circle'; radius: number };
    type Square = { kind: 'square'; size: number };
    type Triangle = { kind: 'triangle'; base: number; height: number };
    
    // Create a discriminated union 'Shape', with the 'kind' property to discriminate the type of object.
    type Shape = Circle | Square | Triangle;
    
    const calculateArea = (shape: Shape) => {
      // ESLint reports that the switch is missing the 'triangle' case
      switch (shape.kind) {
        case 'circle':
          return Math.PI * shape.radius ** 2;
        case 'square':
          return shape.size ** 2;
      }
    };
    
  • 避免使用多个表示互斥状态的布尔标志所带来的代码复杂性。

  • 明确代码意图,通过显式指出给定类型的可能情况,使代码更易阅读和理解。

  • TypeScript 可以收窄联合类型,确保代码在编译时正确。

  • 可辨识联合通过集中定义相关类型,让重构和维护更容易。在联合中添加或修改类型时,编译器会报告代码库中的所有不一致之处。

  • IDE 可以利用可辨识联合提供更好的自动补全和类型推断。

实际应用
必需和可选对象属性 {#required--optional-object-properties}

尽量让大多数对象属性为必需属性,谨慎使用可选属性。

这种做法体现了对类型安全、易于维护的代码的设计理念:

  • 清晰明确 - 必需属性明确指出始终应提供哪些数据。这减少了开发者使用或接收对象时的歧义,因为他们清楚地知道必须包含哪些内容。
  • 类型安全 - 属性为必需属性时,TypeScript 可以强制要求提供这些属性,并在类型检查期间发现缺少的属性。
  • 避免过度使用可选链 - 如果太多属性都是可选的,通常就需要大量使用可选链(?.)来处理可能未定义的值。这会让代码显得杂乱,也会掩盖代码的意图。

当值可能独立缺失时,使用可选属性。当属性是否存在取决于对象的变体时,使用判别联合类型。

// ❌ Avoid optional properties when their presence depends on the variant
type User = {
  id?: number;
  email?: string;
  dashboardAccess?: boolean;
  adminPermissions?: ReadonlyArray<string>;
  subscriptionPlan?: 'free' | 'pro' | 'premium';
  rewardsPoints?: number;
  temporaryToken?: string;
};

// ✅ Use a discriminated union so each variant has only its required properties
type AdminUser = {
  role: 'admin';
  id: number;
  email: string;
  dashboardAccess: boolean;
  adminPermissions: ReadonlyArray<string>;
};

type RegularUser = {
  role: 'regular';
  id: number;
  email: string;
  subscriptionPlan: 'free' | 'pro' | 'premium';
  rewardsPoints: number;
};

type GuestUser = {
  role: 'guest';
  temporaryToken: string;
};

// Discriminated union type 'User' ensures clear intent with no optional properties
type User = AdminUser | RegularUser | GuestUser;

const regularUser: User = {
  role: 'regular',
  id: 212,
  email: '[email protected]',
  subscriptionPlan: 'pro',
  rewardsPoints: 1500,
  dashboardAccess: false, // Error: 'dashboardAccess' property does not exist
};
应用状态

当应用状态需要不同的数据时,使用判别联合将状态及其数据建模在一起。这可以避免无效组合,例如加载状态同时持有数据和错误。

// ❌ Boolean flags and optional properties allow invalid state combinations
type RequestState = {
  isLoading: boolean;
  data?: Products;
  error?: string;
};

// ✅ Each state contains only the data valid for that state
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: Products }
  | { status: 'error'; error: string };
函数参数

当函数接受需要不同属性的互斥变体时,使用判别联合类型。这可以降低函数 API 的复杂度,并确保每种使用场景只传入必需的属性。

// ❌ Avoid optional properties that allow invalid combinations in the function API
type NotificationParams = {
  channel: 'email' | 'sms';
  email?: string;
  phoneNumber?: string;
  subject?: string;
  message: string;
};

// ✅ Use a discriminated union so each variant requires only its valid properties
type EmailNotificationParams = {
  channel: 'email';
  email: string;
  subject: string;
  message: string;
};

type SmsNotificationParams = {
  channel: 'sms';
  phoneNumber: string;
  message: string;
};

type NotificationParams = EmailNotificationParams | SmsNotificationParams;

export const sendNotification = (params: NotificationParams) => {
  switch (params.channel) {
    case 'email':
      return sendEmail(params.email, params.subject, params.message);
    case 'sms':
      return sendSms(params.phoneNumber, params.message);
  }
};
React 属性
必需和可选属性

尽量让大多数属性为必需属性,谨慎使用可选属性。

尤其是在为组件的首次或唯一使用场景创建新组件时,大多数属性都应为必需属性。当组件开始覆盖更多使用场景时,只为那些在这些场景中确实可能缺失的值引入可选属性。
也有一些例外情况,组件 API 可能从一开始就需要实现可选属性(例如,覆盖多个使用场景的共享组件、UI 设计系统组件——按钮的 isDisabled 等)。

如果组件或 Hook 变得过于复杂,可能就应该将其拆分为更小的部分。
一个夸张的例子:实现 10 个各有 5 个必需属性的 React 组件,好过实现一个“无所不能”的组件,让它接受 50 个可选属性。

使用判别类型表示属性

当组件变体需要不同的属性时,使用判别联合类型。这种做法可以降低组件 API 的复杂度,并确保每种变体只传入必需的属性。

// ❌ Avoid optional props that allow invalid combinations in the component API
type AvatarProps = {
  variant: 'image' | 'initials';
  src?: string;
  alt?: string;
  initials?: string;
};

// ✅ Use a discriminated union so each variant requires only its valid props
type ImageAvatarProps = {
  variant: 'image';
  src: string;
  alt: string;
};

type InitialsAvatarProps = {
  variant: 'initials';
  initials: string;
};

type AvatarProps = ImageAvatarProps | InitialsAvatarProps;

export const Avatar = (props: AvatarProps) => {
  switch (props.variant) {
    case 'image':
      return <img src={props.src} alt={props.alt} />;
    case 'initials':
      return <span>{props.initials}</span>;
  }
};

评论

支持 Markdown