Typescript Discriminated Unions logo

Typescript Discriminated Unions

Community
mkosir
typescript-discriminated-unions

Apply, review, and explain discriminated unions in TypeScript. Use automatically for tasks involving mutually exclusive variants, invalid state prevention, exhaustiveness checking, variant-specific object properties, application state, function arguments, or React props.

Overview

Publishermkosir
Repositorytypescript-style-guide
Skill nametypescript-discriminated-unions
Stars
783
Forks
37
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by mkosir on GitHub. Read the source before you install it.

Installation

Install the Typescript Discriminated Unions AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/mkosir/typescript-style-guide/tree/main/skills/typescript-discriminated-unions
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/mkosir/typescript-style-guide.git /tmp/typescript-style-guide
mkdir -p .claude/skills
cp -r /tmp/typescript-style-guide/skills/typescript-discriminated-unions .claude/skills/typescript-discriminated-unions
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Typescript Discriminated Unions in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Typescript Discriminated Unions on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Typescript Discriminated Unions is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Discriminated Unions

Apply the TypeScript Style Guide's discriminated-union conventions in the context of the current task.

Workflow

  1. Inspect the consuming repository's conventions and configuration.
  2. Let explicit repository conventions take precedence over this opinionated guidance.
  3. Apply, review, or explain only the guidance relevant to the task.
  4. State important tradeoffs when the appropriate model depends on context or judgment.

Boundaries

  • Use discriminated unions for mutually exclusive variants that require different data.
  • Do not force a discriminated union when properties may independently be absent or when only a value changes.
  • Keep TypeScript and ESLint responsible for checks they can enforce automatically.
  • Do not introduce unrelated TypeScript Style Guide conventions merely because this skill is active.

Discriminated Unions {#discriminated-unions}

If there's only one TypeScript feature to choose from, embrace discriminated unions.

A discriminated union is a union of object types that share a property with distinct literal values. Checking that property narrows the value to the matching variant.

Use discriminated unions when variants are mutually exclusive and each variant requires different data. Keep properties optional when they may independently be absent, and use a literal union when only the value changes.

Prefer a shared literal discriminator when variants represent named states or modes and you control their shape. Use optional never properties only when property presence is itself the natural distinction and adding a discriminator would make the API less clear.

Discriminated unions are a powerful concept to model complex data structures and improve type safety, leading to clearer and less error-prone code.
You may encounter discriminated unions under different names, such as tagged unions or sum types, in languages such as C, Haskell, and Rust (in conjunction with pattern-matching).

Advantages of discriminated unions:

  • As mentioned in Required & Optional Object Properties, Function Arguments, and Props as Discriminated Type, discriminated unions replace optional properties that depend on a variant with required properties for that variant, reducing complexity.

  • Exhaustiveness Checking - The configured ESLint rule reports when a switch does not handle every variant of a discriminated union.

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

    ts
    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;
      }
    };
  • Avoid code complexity introduced by multiple boolean flags that represent mutually exclusive states.

  • Clear code intent, as it becomes easier to read and understand by explicitly indicating the possible cases for a given type.

  • TypeScript can narrow down union types, ensuring code correctness at compile time.

  • Discriminated unions make refactoring and maintenance easier by providing a centralized definition of related types. When adding or modifying types within the union, the compiler reports any inconsistencies throughout the codebase.

  • IDEs can leverage discriminated unions to provide better autocompletion and type inference.

Practical Applications

Required & Optional Object Properties {#required--optional-object-properties}

Strive to have the majority of object properties required and use optional properties sparingly.

This approach reflects designing type-safe and maintainable code:

  • Clarity and Predictability - Required properties make it explicit which data is always expected. This reduces ambiguity for developers using or consuming the object, as they know exactly what must be present.
  • Type Safety - When properties are required, TypeScript can enforce their presence and catch missing properties during type checking.
  • Avoids Overuse of Optional Chaining - If too many properties are optional, it often leads to extensive use of optional chaining (?.) to handle potential undefined values. This clutters the code and obscures its intent.

Use optional properties when values may independently be absent. When property presence depends on the object's variant, use a discriminated union type.

ts
// ❌ 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: 'lea@user.com',
  subscriptionPlan: 'pro',
  rewardsPoints: 1500,
  dashboardAccess: false, // Error: 'dashboardAccess' property does not exist
};
Application State

When application states require different data, model the state and its data together with a discriminated union. This prevents invalid combinations, such as loading while holding both data and an error.

ts
// ❌ 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 };
Function Arguments

When a function accepts mutually exclusive variants that require different properties, use a discriminated union type. This decreases complexity in the function's API and ensures that only the required properties are passed for each use case.

ts
// ❌ 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 Props
Required & Optional Props

Strive to have the majority of props required and use optional props sparingly.

Especially when creating a new component for its first or single use case, the majority of props should be required. When the component starts covering more use cases, introduce optional props only for values that may genuinely be absent across those use cases.
There are potential exceptions where a component API needs to implement optional props from the start (e.g. shared components covering multiple use cases, UI design system components - button isDisabled etc.)

If a component or hook becomes too complex, it should probably be broken into smaller pieces.
An exaggerated example: implementing 10 React components with 5 required props each is better than implementing one "can do it all" component that accepts 50 optional props.

Props as Discriminated Type

When component variants require different props, use a discriminated union type. This approach reduces complexity in the component API and ensures that only the required props are passed for each variant.

tsx
// ❌ 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>;
  }
};

Frequently asked questions

What does the Typescript Discriminated Unions AI skill do?

Apply, review, and explain discriminated unions in TypeScript. Use automatically for tasks involving mutually exclusive variants, invalid state prevention, exhaustiveness checking, variant-specific object properties, application state, function arguments, or React props.

Why use Typescript Discriminated Unions on TypingMind?

Because you install it once and use it with any model. Typescript Discriminated Unions is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Typescript Discriminated Unions in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/mkosir/typescript-style-guide/tree/main/skills/typescript-discriminated-unions. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Typescript Discriminated Unions?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Typescript Discriminated Unions?

As many as you like. As long as a model supports skills, you can use Typescript Discriminated Unions with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Typescript Discriminated Unions AI skill free?

Yes. It is published on GitHub by mkosir under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇