Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
World desk7 min

Beyond Promise: Designing a Type-Safe Modal API

A type-safe modal API ties each modal's props to its result type, uses tagged unions for every outcome, and defines how dismissal resolves, with React as the illustrative framework.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A type-safe modal API connects three things that usually drift apart: the configuration a caller passes in, the props the modal renders, and the result the caller gets back. The practical goal is that TypeScript rejects a call with the wrong props, types the outcome for each modal, and forces the caller to handle every way the modal can close. Developers often ask how to build a modal that holds up in production. A r/reactjs discussion asked that question in those terms, and the answers there are informal. The type-level tools that make a stronger answer possible are documented in the TypeScript Handbook.

React is used below as an illustration because it is common, but the type patterns are not specific to React. The core design is a result contract, a registry that links each modal to its props and result, and an explicit policy for dismissal.

As an Amazon Associate I earn from qualifying purchases.

What a type-safe modal API has to connect

A modal has three typed surfaces. The input is the data the caller supplies, such as a item name for a delete confirmation. The presentation is the props the modal component renders with. The output is the value returned when the modal closes. A weak API checks only the first, or none of them. A strong one makes the output depend on which modal was opened, so a caller that opens a rename dialog cannot accidentally read a delete-confirmation result.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

TypeScript’s generics are the mechanism for this. The TypeScript Handbook’s “Generics” chapter states the broader goal: “A major part of software engineering is building components that not only have well-defined and consistent APIs, but also are reusable.” Generics let one function work across many types while keeping the relationship between inputs and outputs visible to the caller.

Model the result as a tagged union

Begin with the result, not the modal. A modal can finish in several distinct ways: the user saves, confirms, cancels, or closes it with the escape key. Represent each as a member of a union with a literal kind field. The Unions and Intersection Types documentation describes how a union value can be narrowed by a shared discriminant.

type SaveResult =
  | { kind: "saved"; value: string }
  | { kind: "cancelled" };

function report(result: SaveResult): void {
  if (result.kind === "saved") {
    console.log(result.value);   // narrowed: value is string here
  } else {
    console.log("No change");    // narrowed: result is the cancelled member
  }
}

The same union also enforces completeness. When a caller switches on kind, adding a new outcome to the union should produce an error in every switch that does not handle it. Add an exhaustiveness check in the default branch:

function label(result: SaveResult): string {
  switch (result.kind) {
    case "saved":
      return result.value;
    case "cancelled":
      return "";
    default: {
      const unreachable: never = result;
      return unreachable;
    }
  }
}

If someone later adds { kind: "discarded" } to the union, the assignment to never fails at compile time until the new branch is handled.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
TypeScript Programming Language - Software Engineer & Coder T-Shirt
  • TypeScript implements a superset of syntax for strictly typed development, facilitating deep static analysis and enhanced development environment integration. The compiler translates source into standard script formats, ensuring parity across any runtime.
  • TypeScript is ideal for front-end developers, full-stack engineers, and software architects who build large-scale web applications. It serves those looking to improve code excellence, reduce bugs through static checking, and maintain complex projects more.
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem

Link props and results with a registry

Once each modal has a result union, connect it to the modal’s props. One common shape is a registry type that maps each modal key to its props and result. The generic signature then derives both types from the key. This is a design option, not an established standard; the TypeScript documentation supplies the indexed-access and generic primitives it relies on, not this exact API.

type ModalRegistry = {
  confirmDelete: {
    props: { itemName: string };
    result: { kind: "confirmed" } | { kind: "cancelled" };
  };
  rename: {
    props: { initial: string };
    result: { kind: "saved"; value: string } | { kind: "cancelled" };
  };
};

type ModalKey = keyof ModalRegistry;
type PropsOf<K extends ModalKey> = ModalRegistry[K]["props"];
type ResultOf<K extends ModalKey> = ModalRegistry[K]["result"];

// Design sketch: the signature is one option among several.
declare function show<K extends ModalKey>(
  key: K,
  props: PropsOf<K>
): Promise<ResultOf<K>>;

async function renameFlow(): Promise<void> {
  const outcome = await show("rename", { initial: "Draft" });
  if (outcome.kind === "saved") {
    console.log(outcome.value.toUpperCase());
  }
  // show("rename", { initial: 42 })  is a compile error: wrong props
  // show("renam", { initial: "" })   is a compile error: unknown key
}

Two details matter here. First, the result type for a modal with nothing to return should not be left implicit. A generic default such as R = void makes that choice explicit, as described in the Generics chapter, which covers generic parameter defaults.

Second, if a wrapper function is async or wraps another promise, the derived result type must match what await actually produces. The built-in Awaited<T> utility, documented in the Utility Types reference, unwraps promise-like types recursively:

type Resolved = Awaited<Promise<Promise<number>>>; // number

Decide dismissal before writing the modal

Escape key, backdrop click, the close button, and unmounting the host component are all ways a modal ends without a deliberate choice. The API has to say what each one returns. The TypeScript sources explain how to type promises and unions, but they do not decide which policy is correct. The following options are design choices, each with a different cost for callers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Policy What the caller receives Effect on types Main trade-off
Tagged cancellation { kind: "cancelled" } Cancellation is a union member that narrowing and exhaustiveness checks cover Every caller must handle the branch, which is verbose but explicit
Optional result undefined or null The result becomes T | undefined Ambiguous when T can itself be undefined
Rejection The promise rejects The success type is unchanged; the error is typed unknown in catch clauses under strict settings A user dismissal looks like a failure and is easy to leave uncaught

The tagged cancellation option is the one that best fits the union pattern above, because the cancellation becomes part of the contract rather than an error path.

Unmount needs its own rule. A promise returned for a modal that is still open when its host unmounts will not settle unless the implementation resolves it. Decide in advance whether unmount resolves the promise as a cancellation, which is a common choice, or does something else, and document it next to the other dismissal behavior.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Typing modal content in React

The modal’s children are the most common place where types are either too loose or too strict. React’s Using TypeScript guide illustrates a modal renderer whose props are typed as title: string and children: React.ReactNode.

type ModalRendererProps = {
  title: string;
  children: React.ReactNode;
};

Use React.ReactNode for flexible content slots

React.ReactNode accepts the range of values React can render, including strings, numbers, JSX elements, arrays, and empty values. For a general-purpose modal body, it is usually the right slot type because callers can pass text, a form, or a mixed layout without conversion.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use React.ReactElement when primitives are not valid

React.ReactElement means a JSX element and excludes primitive strings and numbers. Choose it when the slot must be an element, for example when the modal wraps its content in a layout that expects an element to clone or measure. The trade-off is that callers who want to pass a plain string must wrap it.

Know what TypeScript cannot enforce

The React guide notes that TypeScript cannot express that children must be a particular kind of JSX element. Neither ReactNode nor ReactElement guarantees that a modal receives a specific component as its child. If a modal truly requires a particular child, enforce it through a dedicated prop, a separate component per variant, or a runtime check that reports a clear error in development. Do not rely on the child type alone.

Promise-based versus declarative open and close

The imperative style, where code calls show(...) and awaits a result, and the declarative style, where a parent renders a component with open and onClose props, are both workable. The comparison below lists the axes that matter for typing. The TypeScript and React sources cited here do not rank the two, and the React context row in particular depends on how your application mounts modals.

Axis Promise-oriented imperative API Declarative open and onClose component
Result delivery Returned to the caller through await Passed back through a callback or through state the parent owns
Cancellation A union member or a documented rejection Usually a callback fired on close; its shape depends on how the callback is typed
Exhaustiveness Checked for every branch the caller handles when the result is a union Checked only when the callback argument is itself a union
Props and result association Kept by a registry or generic signature Kept by the component’s own prop types
Participation in React context Not settled by the cited sources; depends on how the host renders the modal Renders inside the component tree and reads context where it is placed

The declarative form keeps the modal inside the component tree, which is convenient for context-dependent content. The promise form makes the flow read top to bottom, which suits multi-step confirmations. Either can meet the typing goals above if the result union and dismissal policy are defined.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Implementation checklist

  • Define one result union per modal, and include cancellation as a tagged member.
  • Key each modal in a registry or generic signature so its props and result types stay linked.
  • Apply one dismissal policy to escape, backdrop click, the close button, and unmount, and document it.
  • Type content slots as React.ReactNode unless only JSX elements are valid.
  • Add a never check to every switch over the result kind.
  • Keep a small set of compile-only checks that show wrong props and unknown keys fail, using @ts-expect-error on the invalid calls.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.