Discriminated unions, satisfies, and template literal types that make your function signatures self-documenting — and catch bugs before they ship.
Good types are not about pleasing the compiler — they are documentation that cannot go out of date and a test suite that runs on every keystroke. Three features do most of the heavy lifting.
A loading flag, an error string, and a data object as three separate fields invites impossible combinations (loading and error at once). Model the result as a discriminated union instead:
type Result =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; data: User };
Now the compiler forces you to handle every case, and data simply does not exist until status is 'success'.
Annotating a config with : Config widens its type and loses literal inference. Using satisfies Config validates the shape and keeps the precise literal types — the best of both worlds.
Route paths, event names, and feature flags are just strings — until a typo ships. Template literal types turn them into checked values:
type Event = `user:${'login' | 'logout' | 'signup'}`;
Aim for types that make the wrong code fail to compile, not types that merely describe the right code.