Skip to content

J0m1ty/ts-safe-union

Repository files navigation

ts-safe-union

npm version Build Status License: MIT

A TypeScript utility for destructuring discriminated unions while maintaining type safety. This package is tiny (no dependencies) and solves a common problem in Typescript. Broadly, this package helps with:

  • Safely destructuring discriminated unions
  • Making switch/case statements exhaustive
  • Improving state management patterns
  • Handling API responses more safely
  • Reducing boilerplate type guards

Installation

npm install ts-safe-union

Motivation

In TypeScript, when working with discriminated unions, you normally can't destructure properties that don't exist on all members of the union:

// Before: Normal TypeScript union
type StateA = { state: "A"; A: number };
type StateB = { state: "B"; B: string };
type State = StateA | StateB;

// Error: Property 'B' does not exist on type 'StateA'
const { state, A, B } = someState;

Helpfully, ts-safe-union was designed to improve this behavior:

// After: Using DiscriminatedUnion
type State = DiscriminatedUnion<
  "state",
  {
    A: { A: number };
    B: { B: string };
  }
>;

// Works! Properties are safely destructurable
const { state, A, B } = someState;

Usage

DiscriminatedUnion

Use DiscriminatedUnion when you have a well-defined discriminating property (e.g. status, type, etc.) or you have many states.

import { DiscriminatedUnion } from "ts-safe-union";

// Define your discriminated union type
type RequestState = DiscriminatedUnion<
  "status", // The discriminator property name
  {
    // Each key defines a variant with its properties
    loading: { progress: number };
    success: { data: unknown };
    error: { error: Error };
  }
>;

// Example request consumer
const handleRequest = (request: RequestStateWithCommon) => {
  const { status, progress, data, error } = request;

  if (status === "loading") {
    console.log(`Loading: ${progress}%`);
  } else if (status === "success") {
    console.log(`Success: ${JSON.stringify(data)}`);
  } else {
    console.log(`Error: ${error.message}`);
  }
};

If you want to define your keys elsewhere, you can provide them to the Discriminated Union and the TS complier will give you errors if some variant are not supplied in the list. For example, the following example will have a type error because the error variant is not specified.

import { DiscriminatedUnion } from "ts-safe-union";

const statusKeys = ["loading", "success", "error"] as const;

type RequestState = DiscriminatedUnion<
  "status",
  {
    loading: { progress: number };
    success: { data: unknown };
  },
  typeof statusKeys[number] // << error here
>;

MergedUnion

Use MergedUnion when you want to merge two existing object types into a single union while maintaining safe property access.

import { MergedUnion } from "ts-safe-union";

// Define your individual state types
type Success = { state: "success"; data: unknown };
type Error = { state: "error"; error: Error };

// Merge them into a union type
type RequestState = MergedUnion<Success, Error>;

const handleRequest = (request: RequestState) => {
  const { state, data, error } = request;

  if (state === "success") {
    console.log(`Success: ${JSON.stringify(data)}`);
  } else {
    console.log(`Error: ${error.message}`);
  }
};

More Examples

Check out the examples directory for simple but practical use cases:

Contributing

Contributions are welcome! Please feel free to submit a PR. If you feel comfortable, also:

  1. Add tests for any new features
  2. Update documentation if needed
  3. Ensure all tests pass by running npm test

License

MIT © Jomity

About

TypeScript utility for safely destructuring discriminated unions

Topics

Resources

License

Stars

Watchers

Forks