๐—ง๐˜†๐—ฝ๐—ฒ๐—ฆ๐—ฐ๐—ฟ๐—ถ๐—ฝ๐˜ ๐——๐—ถ๐˜€๐—ฐ๐—ฟ๐—ถ๐—บ๐—ถ๐—ป๐—ฎ๐˜๐—ฒ๐—ฑ ๐—จ๐—ป๐—ถ๐—ผ๐—ป๐˜€

I once spent three hours debugging a single error.

The error said a property did not exist on a type. My logic was correct. The problem was my type definition. I used a wide union. TypeScript could not verify the shape of my data.

This is when I learned about discriminated unions.

Many developers write code like this:

type APIResponse = | { data: User[]; error: null; status: "success" } | { data: null; error: string; status: "error" } | { data: null; error: null; status: "loading" };

This looks fine. But TypeScript cannot narrow the type easily in if blocks. If you check for data, you rely on truthiness. If data is an empty array, your logic fails. This leads to runtime bugs.

The fix is simple. Use one shared field with a unique literal value. This field is often called kind or status.

type APIResponse = | { kind: "success"; data: User[] } | { kind: "error"; statusCode: number; message: string } | { kind: "loading" } | { kind: "idle" };

Now TypeScript understands your code. When you use a switch statement on the kind field, the type narrows automatically.

In the success case, TypeScript knows the data exists. In the error case, it knows the status code exists. You get full type safety in every branch.

This pattern works for:

You can also use an assertNever helper to catch mistakes. If you add a new state to your type but forget to update your switch statement, TypeScript will show an error immediately. This prevents bugs before you even run your code.

Stop using generic strings for types. Use literal values. It saves time and prevents hours of debugging.

Source: https://dev.to/kaithorne/typescript-discriminated-unions-the-pattern-that-made-my-error-handling-click-2550