npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bender-tools/react-discriminated-union-context

v1.3.4

Published

A TypeScript library for creating type-safe discriminated union contexts in React

Readme

@bender-tools/react-discriminated-union-context

A TypeScript library for creating type-safe React contexts with discriminated unions. This library provides full type narrowing support when accessing context values based on their discriminant.

Installation

npm install @bender-tools/react-discriminated-union-context

Features

  • 🔒 Type-safe: Full TypeScript support with automatic type narrowing
  • 🎯 Discriminated Unions: Perfect for state machines and multi-state contexts
  • Lightweight: Zero dependencies (besides React peer dependency)
  • 📦 ESM Only: Modern ES modules support

Usage

Basic Example

import { createDiscriminatedContext } from "@bender-tools/react-discriminated-union-context";

// Define your discriminated union type
type AuthState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "authenticated"; user: { name: string; email: string } }
  | { status: "error"; error: string };

// Create the context with the discriminant key
const { Context: AuthContext, useContext: useAuthContext } =
  createDiscriminatedContext<AuthState, "status">("status");

// Use in a provider
function AuthProvider({ children }: { children: React.ReactNode }) {
  const [auth, setAuth] = useState<AuthState>({ status: "idle" });

  return (
    <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>
  );
}

// Use in components - type is automatically narrowed!
function UserProfile() {
  // When you pass the expected discriminant value, the type is narrowed
  const auth = useAuthContext("authenticated");
  // auth is typed as: { status: 'authenticated'; user: { name: string; email: string } }

  return <div>Welcome, {auth.user.name}!</div>;
}

// Use 'default' to get the full union type without narrowing
function AuthStatus() {
  const auth = useAuthContext("default");
  // auth is typed as: AuthState (the full union)

  switch (auth.status) {
    case "idle":
      return <div>Not started</div>;
    case "loading":
      return <div>Loading...</div>;
    case "authenticated":
      return <div>Logged in as {auth.user.name}</div>;
    case "error":
      return <div>Error: {auth.error}</div>;
  }
}

// With 'default', you can also destructure properties from any union member
function AuthStatusWithDestructuring() {
  const auth = useAuthContext("default");
  // Destructure all possible properties - hover over 'user' or 'error'
  // to see hints like: `Use useContext("authenticated") to access "user"`
  const { status, user, error } = auth;

  // Note: 'user' and 'error' may be undefined depending on current state
  return (
    <div>
      <p>Status: {status}</p>
      {status === "authenticated" && user && <p>User: {user.name}</p>}
      {status === "error" && error && <p>Error: {error}</p>}
    </div>
  );
}

Runtime Validation

When you specify an expected discriminant value, the hook will throw an error at runtime if the actual value doesn't match:

function UserProfile() {
  // This will throw if auth.status !== 'authenticated'
  const auth = useAuthContext("authenticated");

  return <div>Welcome, {auth.user.name}!</div>;
}

This is useful for components that should only render in specific states, catching bugs early in development.

API

createDiscriminatedContext<TUnion, TDiscriminant>(discriminantKey)

Creates a discriminated context with type-safe narrowing support.

Parameters

  • discriminantKey: The key used as the discriminant in your union type

Returns

  • Context: The React Context object (for use with Context.Provider)
  • useContext: A hook to consume the context with required type narrowing. Pass a discriminant value to narrow the type, or 'default' to get the full union type.

Throws

  • Error if useContext is called outside of a Provider

DiscriminantValues<TUnion, TKey>

A utility type that extracts all possible values of the discriminant key from a union type.

type AuthState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "authenticated"; user: User };

type StatusValues = DiscriminantValues<AuthState, "status">;
// Result: 'idle' | 'loading' | 'authenticated'

Example Application

The repository includes a full example React application demonstrating the library in action.

Running the Example

# Clone the repository
git clone https://github.com/ScriptAlchemist/react-discriminated-union-context.git
cd react-discriminated-union-context

# Build the library
npm install
npm run build

# Run the example app
cd example
npm install
npm run dev

Then open http://localhost:5173 in your browser.

Example Structure

The example demonstrates an authentication state machine:

example/src/
├── authContext.ts          # Context and types definition
├── components/
│   ├── AuthStatus.tsx      # Uses full union type
│   ├── UserProfile.tsx     # Narrowed to "authenticated"
│   └── ErrorDisplay.tsx    # Narrowed to "error"
├── App.tsx                 # Main app with provider
└── main.tsx                # Entry point

Key Patterns Demonstrated

  1. Shared Context Module (authContext.ts):

    import { createDiscriminatedContext } from "@bender-tools/react-discriminated-union-context";
    
    export type AuthState =
      | { status: "idle" }
      | { status: "loading" }
      | {
          status: "authenticated";
          user: { name: string; email: string };
        }
      | { status: "error"; error: string };
    
    export const { Context: AuthContext, useContext: useAuthContext } =
      createDiscriminatedContext<AuthState, "status">("status");
  2. Full Union Type Usage (AuthStatus.tsx):

    export function AuthStatus() {
      const auth = useAuthContext("default"); // Type: AuthState
      // Handle all cases manually
    }
  3. Destructuring with 'default' - Access all possible properties:

    export function AuthStatus() {
      const auth = useAuthContext("default");
      // Destructure properties from any union member
      // Hover over 'user' or 'error' to see narrowing hints
      const { status, user, error } = auth;
    
      // TypeScript hints: `Use useContext("authenticated") to access "user"`
      console.log("status:", status);
      console.log("user:", user); // May be undefined
      console.log("error:", error); // May be undefined
    }
  4. Type Narrowing (UserProfile.tsx):

    export function UserProfile() {
      const auth = useAuthContext("authenticated");
      // Type: { status: "authenticated"; user: { name: string; email: string } }
      return <div>{auth.user.name}</div>; // TypeScript knows auth.user exists!
    }
  5. Required Parameter:

    // ❌ Error - empty () not allowed, must specify a value
    const auth = useAuthContext();
    
    // ✅ Use 'default' to get full union type
    const auth = useAuthContext("default");
    
    // ✅ Use a discriminant value to narrow the type
    const auth = useAuthContext("authenticated");

Requirements

  • React 18.0.0 or higher (including React 19)
  • TypeScript 5.0 or higher (for best type inference)

Running Tests

This project includes a comprehensive test suite using the Node.js built-in test runner and @testing-library/react.

From the repository root, you can run:

# Install dependencies
npm install

# Type-check the library source
npm run typecheck

# Type-check the test files (uses tsconfig.test.json)
npm run typecheck:test

# Run the test suite in watch/interactive mode (if configured)
npm test

# Run the test suite in CI mode (no watch, suitable for pipelines)
npm run test:ci

The tests cover:

  • Type narrowing behavior for discriminated unions
  • 'default' behavior (including that it does not validate the discriminant)
  • Error handling when used outside of a Provider
  • Various discriminant types and runtime validation behavior

License

MIT