vs Discriminated Unions

Sealed Classes Versus TypeScript Discriminated Unions

TypeScript models the same state as a discriminated union: object types that share a literal status field. Exhaustiveness is opt-in, through a default branch that assigns the value to never. Here the loading case is missing, and tsc --strict (TypeScript 7.0.2) reports it.

The same LoadState as a TypeScript discriminated union (state.ts)TypeScript
type LoadState<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };
function render(state: LoadState<string[]>): string {
  switch (state.status) {
    case "success": return state.data.join(", ");
    case "error": return `${state.message} [Retry]`;
    default: { const unhandled: never = state; return unhandled; }
  }
}
Output
state.ts(9,22): error TS2322: Type '{ status: "loading"; }' is not assignable to type 'never'.
Sealed types compared with discriminated unions
Aspect Kotlin sealed type TypeScript union
Case identity A class per case, tested with is A literal tag field
Exhaustiveness Built into when Opt-in never check
At run time Real JVM classes Types erased; only the tag remains
Conditional cases Guards: is X if ... Extra if or a matching library

For Kotlin-style matching in TypeScript, the MIT-licensed ts-pattern (https://github.com/gvergnaud/ts-pattern 15,170 ) (npm 2,036 install ts-pattern, 5.9.0) adds match(state).with(...).exhaustive().