{
  "id": 9783529,
  "title": "Beyond Promise<any>: Designing a Type-Safe Modal API",
  "url": "https://urgent.news/2026/09/25/beyond-promise-any-designing-a-type-safe-modal-api",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-09-25T15:33:40.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/alexey79/beyond-promise-designing-a-type-safe-modal-api-1248"
  },
  "original_language": "en",
  "account": "The abstract model for a modal interaction is an asynchronous operation: input to the modal, resulting in a result. This leads to an API where a modal is opened with a type of input and a promise is awaited for the resulting type. However, this approach does not ensure the contract is enforced. The question remains: what type does 'result' have? If the answer is 'any', the control flow is improved, but the contract remains unchecked.\n\nA modal manager can expose a promise API and still lose information. Consider this example:\n\nconst result = await showModal('rename-report', { reportId: report.id });\n\nHere, the 'result' variable is of type 'any'. This allows any number of expressions to compile:\n\nresult.name;\nresult.nmae;\nresult.whatever;\n\nThe compiler cannot determine if the modal returns 'name'; if the field was renamed; if the caller made a typo; or if the modal returns a completely different shape. The promise itself is not the crucial part. The crucial part is preserving the relationship between the modal definition, input type, result type, and call site.\n\nTo address this, start with a generic Modal<TInput, TResult>. For instance, a rename modal receives an interface RenameReportInput with properties 'reportId' and 'currentName', both of type string. The modal can produce one of two domain outcomes: a 'renamed' result with a new name, or a 'cancelled' result. This modal conceptually is:\n\nModal<RenameReportInput, RenameReportResult>\n\nThe producer, or the modal component, receives typed input and a typed close function. In React Modal Manager, the component receives the typed input and close function:\n\nfunction RenameReportModal({ input, close }: ModalComponentProps<RenameReportInput, RenameReportResult>)\n\nInside the component, 'input.currentName' is known to be a string, and 'close()' only accepts a valid RenameReportResult. Attempting to close with an invalid status or missing properties results in a compile-time error.\n\nThe producer side of the interaction is checked. To carry the contract further, create the modal definition layer. This ensures the relationship between modal definition, input type, result type, and call site is preserved throughout the API.",
  "summary": "await modal.open(...) improves control flow. But if the result is any , the most important part of the contract is still unchecked. In the previous article , I described a modal interaction as an asynchronous operation: Input ↓ [ user interaction ] ↓ Result That naturally leads to an API like: const result = await modal . open ( renameReportModal , input ); That immediately raises the question…",
  "key_points": [
    "Modal API designed as asynchronous operation with input and promise for result.",
    "Type safety enforced by generic Modal TInput, TResult structure.",
    "Producer and consumer sides of interaction are checked for contract preservation."
  ],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}