Recommended Free Tools
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.
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.
#1 Best Overall
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.
Rank #2
- 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.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware match| 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.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.
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.
Best Value
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.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Quick Recap
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.ReactNodeunless only JSX elements are valid. - Add a
nevercheck 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-erroron 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.




