6 Non-Obvious TypeScript Tricks for React Devs
· Tutorials
Six non-obvious TypeScript patterns for React — discriminated unions, template literals, satisfies, and more. Real code examples from production codebases.
Last updated: July 27, 2026 · 5-minute read
A thread on r/typescript last month asked for "non-obvious" TS patterns. Most responses were the usual generics tutorial stuff — conditional types, mapped types, the basics. Here are six patterns I actually use every day in React codebases. None of them are in the official handbook's beginner section, and none of them require a PhD in type theory.
Discriminated Unions for Component State
This is the single most impactful pattern I adopted in the last year. Instead of optional fields and boolean flags, use a discriminated union to model every possible state of a component explicitly.
type UserState = | { status: "idle" } | { status: "loading" } | { status: "success"; user: User } | { status: "error"; error: string };
The benefit is that TypeScript narrows the type automatically inside each branch. When status is "success", the user property is guaranteed to exist — no null checks, no assertion operators, no ! scattered around.
In your component, this forces you to handle every state:
If you add a new state later, TypeScript will flag every switch statement that does not handle it. That is the kind of safety net that prevents bugs in production.
Template Literal Types for Route Params
If you are building a router or defining API routes, template literal types prevent typos at the type level.
type ExtractParams<T extends string = T extends :\${infer Param}/\${infer Rest} ? Param | ExtractParams<Rest : T extends :\${infer Param} ? Param : never;
type Params = ExtractParams<Route;
This ensures your route handler functions accept exactly the params that each route actually declares. Someone on X posted a similar pattern and called it "type-safe routing without a library" — which is exactly what it is.
The satisfies Operator
Introduced in TypeScript 4.9, satisfies validates that a value matches a type without widening it. This is subtly different from a type annotation.
The real power shows up with const contexts and object types that have literal value types:
I use this for configuration objects across my projects — it catches missing keys at write time while preserving literal types for downstream usage.
Const Assertions for API Configs
The as const suffix freezes every value in an object to its literal type. This is how I define action types, status codes, and event names.
type StatusCode = (typeof HttpStatus)[keyof typeof HttpStatus];
Without as const, every value would be typed as number, and you lose the ability to narrow on specific status codes.
Utility Types: Pick, Omit, Record
Most people know these exist but underestimate how much they simplify prop drilling. The specific pattern I use constantly is extracting component prop types from larger data models.
type ProfileProps = Pick<User, "id" | "name" | "email";
type SafeUser = Omit<User, "passwordHash";
type UserById = Record<string, User;
This pattern becomes essential when you have a single data model that gets passed through five layers of components, each needing a different subset of fields. A post on r/typescript with 890 upvotes called Omit "the most underrated utility type" and I agree — it solves the "I want all of this except one field" problem cleanly.
ReturnType for Async Actions
When you have an async function that returns complex data, Awaited<ReturnType<fn gives you the unwrapped return type.