Advanced TypeScript Patterns Every Developer Should Know
I spent my first year with TypeScript treating it like JavaScript with type annotations. string here, number there, maybe an interface if I was feeling fancy. Then I discovered TypeScript's type system could actually prevent entire categories of bugs before they happened.
Here's what I wish someone had shown me back then—the patterns that transformed how I write production code in 2025.
Why This Matters (And Why AI Makes It Easier)
TypeScript 5.x has become incredibly sophisticated. The type system can express complex relationships that would've required runtime validation just a few years ago. But here's the thing: you don't need to memorize all these patterns.
With modern AI assistants, I'll often ask: "Help me create a type that extracts the return type from this async function" or "Show me how to type a discriminated union for these API responses." The AI shows me the pattern, I understand the concept, and boom—type safety that would've taken hours to figure out on my own.
That said, knowing when to use these patterns is still on you. Let's dive in.
Conditional Types: The Game Changer
Think of conditional types as if/else statements for your type system. I use them constantly for API responses, form handling, and anywhere the shape of data depends on runtime conditions.
Here's the real-world scenario that made this click for me: I had an API that sometimes returned data, sometimes errors. TypeScript kept complaining because it couldn't know which properties existed.
The solution? A conditional type that changes based on success/failure:
type ApiResponse<T> = T extends { error: infer E }
? { success: false; error: E }
: { success: true; data: T };
Now when I handle the response, TypeScript knows what properties exist:
const result = await fetchUser("123");
if (result.success) {
console.log(result.data.name); // ✓ TypeScript knows data exists
} else {
console.error(result.error); // ✓ TypeScript knows error exists
}
No more defensive coding with ?. everywhere. The types guide you to handle both cases.
AI prompt that helped me: "Create a TypeScript conditional type for API responses that either return data or an error object."
Mapped Types: Transform Your Interfaces
Mapped types let you transform existing types by iterating over properties. I discovered this when building forms—every field needed the same wrapper structure (value, error state, touched state), but I didn't want to manually type each one.
type FormState<T> = {
[K in keyof T]: {
value: T[K];
error?: string;
touched: boolean;
};
};
This pattern saved me from writing hundreds of lines of repetitive type definitions. For a User interface with 10 fields, TypeScript automatically generates the complete form state type.
The beauty? When you add a field to User, the form state updates automatically. No synchronization bugs.
What I learned: Start simple. Don't create mapped types until you find yourself copy-pasting type definitions. Then refactor to a mapped type.
Template Literal Types: Type-Safe Strings
This one blew my mind. You can manipulate string literal types at the type level.
I use this for CSS-in-JS libraries, API route definitions, and event systems. Here's a simplified example for type-safe routes:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = "/users" | "/posts" | "/comments";
type Endpoint = `${HTTPMethod} ${Route}`;
Now Endpoint is a union of all valid combinations: "GET /users", "POST /users", etc. Your API client won't let you request "GET /invalid".
In 2025, tools like Cursor and VS Code Copilot have gotten really good at generating these. I'll describe what I need in plain English, and they'll scaffold the template literal type. I just verify it makes sense.
Related reading: If you're building Next.js apps, check out my post on type-safe routing in React for more patterns like this.
Discriminated Unions: Model Complex State
This is the pattern for state machines, action creators, and anything with distinct states.
The key is a literal property (usually type) that TypeScript uses to narrow the union:
type Action =
| { type: "FETCH_REQUEST" }
| { type: "FETCH_SUCCESS"; payload: User[] }
| { type: "FETCH_ERROR"; error: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FETCH_SUCCESS":
// TypeScript knows action.payload exists here
return { ...state, users: action.payload };
case "FETCH_ERROR":
// TypeScript knows action.error exists here
return { ...state, error: action.error };
}
}
Before I learned this pattern, I used optional properties everywhere. payload?: User[], error?: string. Then I'd forget to check if they existed and get runtime errors.
Discriminated unions force you to handle each case explicitly. Exhaustiveness checking (default: never) catches cases you forgot.
AI-assisted debugging: When my discriminated union isn't working, I paste the type and error into Claude/GPT and ask: "Why isn't TypeScript narrowing this union correctly?" Usually it's missing as const or the discriminant isn't actually literal.
The Infer Keyword: Extract Types
infer lets you extract types from complex structures. I mostly use it for two things:
- Extracting function return types (especially async functions)
- Unwrapping Promises
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
async function fetchUsers() {
return [{ id: 1, name: "John" }];
}
type Result = Awaited<ReturnType<typeof fetchUsers>>; // User[]
Honestly, I don't write these from scratch anymore. TypeScript's built-in utility types (ReturnType, Awaited, etc.) cover 90% of cases. When I need something custom, I ask an AI assistant and it generates it.
What matters: Understanding that you can extract types from complex structures. The syntax is forgettable (and AI can remind you).
Modern TypeScript in 2025: What's New
TypeScript 5.x brought some incredible features:
satisfiesoperator: Validate without widening types- Better type inference: Less explicit typing needed
- Decorators: Now standardized (ES2023)
consttype parameters: For improved generic inferenceusingdeclarations: Explicit resource management with type safety
The biggest shift? TypeScript now feels less like fighting the compiler and more like having a pair programmer who catches your mistakes.
Using AI for TypeScript (My Workflow)
Here's how I actually work with TypeScript in 2025:
- Write the logic first: Get the code working without perfect types
- Let TypeScript yell at you: See where type errors occur
- Ask AI for help: "How do I type this generic React component?"
- Review and understand: Don't just copy-paste—understand what it generated
- Refactor: Improve the types as you learn more about the domain
Example prompts I use regularly:
- "Create a TypeScript type for this JSON API response: [paste response]"
- "How do I make this function generic while preserving type information?"
- "Type this React form hook to track field values and validation errors"
- "Fix this TypeScript error: [paste error message and relevant code]"
The AI shows patterns, but you decide if they fit your use case.
Patterns to Avoid (Lessons Learned)
Over-engineering Types
Early on, I created elaborate type hierarchies because I could. Generics within generics, complex conditional types, the works. Then I spent hours debugging type errors.
What I learned: Start simple. Add complexity only when you're repeatedly solving the same problem. Most code doesn't need advanced patterns.
Type Gymnastics for Edge Cases
Sometimes you'll have a case that's hard to type. Maybe it's a dynamic property access, or some runtime magic that TypeScript can't express.
It's okay to use as or // @ts-expect-error with a comment explaining why. Don't spend 4 hours creating a perfect type for code that's called twice.
Ignoring Inference
TypeScript's type inference is exceptional in 2025. Don't annotate everything:
// ✗ Redundant
const users: User[] = await fetchUsers();
// ✓ TypeScript infers User[] from fetchUsers return type
const users = await fetchUsers();
Let the compiler do its job. Add types when inference fails or for public APIs.
Tools That Help
In 2025, the TypeScript ecosystem is mature:
- VS Code / Cursor: Inline type errors, autocomplete, refactoring
- ts-reset: Better type defaults for DOM and built-ins
- type-fest: Utility types for common patterns
- ESLint: Catch TypeScript anti-patterns
- AI assistants: Claude, GPT, Cursor AI for pattern generation
I keep TypeScript's utility types bookmarked and ask AI when I need something custom.
Wrapping Up
Advanced TypeScript patterns aren't about showing off—they're practical tools that prevent bugs and improve the development experience.
My advice? Learn discriminated unions and conditional types first. They solve the most common problems. Then explore mapped types and template literals as needed.
And remember: AI assistants are fantastic at generating TypeScript patterns. Use them. The key is understanding when to use each pattern, not memorizing the syntax.
What's your go-to TypeScript pattern? Hit me up on Twitter or check out my other posts on async/await pitfalls and type safety in React.
Using TypeScript with large codebases? Check out my guide on TypeScript best practices for large projects for architectural patterns that scale.