Advanced TypeScript Patterns
Production-grade TypeScript patterns for interviews.
📘 Advanced TypeScript Patterns (Production + Interview Focused)
Table of Contents
- Type Narrowing Patterns
- Exhaustive Checking with never
- Discriminated Unions
- Generic Utility Patterns
- Conditional Types
- Mapped Types in Production
- Keyof + Lookup Types
- Function Overloads in Real Code
- Type-safe Event Systems
- React-Specific Advanced Patterns
- API Response Safety Pattern
- Assertion Functions
- Preventing Invalid States
- Branded Types
- When NOT to Use Advanced Types
- What Interviewers Actually Look For
- One-Line Senior Summary
1. Type Narrowing Patterns (Critical for Interviews)
1.1 typeof Narrowing
function format(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}Why it matters:
- Eliminates runtime errors
- Compiler understands control flow
1.2 in Operator Narrowing
type Admin = { role: "admin"; permissions: string[] };
type User = { role: "user"; name: string };
function handleAccount(account: Admin | User) {
if ("permissions" in account) {
account.permissions.push("write");
}
}Used heavily with:
- API responses
- Role-based logic
1.3 instanceof Narrowing
class ApiError extends Error {
statusCode: number = 500;
}
function handleError(error: unknown) {
if (error instanceof ApiError) {
console.log(error.statusCode);
}
}Flowchart: Which Narrowing to Use
Pick the check by what you can test at runtime, and the compiler narrows the type inside the branch.
2. Exhaustive Checking with never (Very Important)
Pattern
type Status = "loading" | "success" | "error";
function assertNever(x: never): never {
throw new Error("Unhandled case");
}
function render(status: Status) {
switch (status) {
case "loading":
return "Loading...";
case "success":
return "Success!";
case "error":
return "Error!";
default:
return assertNever(status);
}
}Why interviewers love this:
- Guarantees future safety
- Compiler error when new union member is added
Flowchart: How the never Check Works
After every case is handled, nothing is left for default, so its type is never.
3. Discriminated Unions (Most Asked Advanced Topic)
Core Pattern
type ApiResponse =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };Usage:
function render(response: ApiResponse) {
switch (response.status) {
case "success":
return response.data.map((item) => item);
case "error":
return response.error;
}
}Production usage:
- API state management
- Redux reducers
- UI state machines
State diagram: A Union as a State Machine
Each variant of the union is one state, and only one is active at a time.
4. Generic Utility Patterns
4.1 Identity + Constraint Pattern
function byId<T extends { id: string }>(items: T[]): Record<string, T> {
return Object.fromEntries(items.map((i) => [i.id, i]));
}Why it matters:
- Type-safe helpers
- Reusable logic
- No runtime checks needed
4.2 Generic Default Types
type ApiResult<T = unknown> = {
data: T;
error?: string;
};Used in:
- SDKs
- API wrappers
- Hooks
5. Conditional Types (Advanced but Real)
Basic Conditional Type
type IsString<T> = T extends string ? true : false;Example:
type A = IsString<string>; // true
type B = IsString<number>; // falseConditional Types with Inference (infer)
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;This is how built-in ReturnType works.
Flowchart: Conditional Types
A conditional type is an if over types, and infer captures a piece of the matched type.
6. Mapped Types in Production
6.1 Readonly Mapping
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};6.2 Optional Mapping
type Optional<T> = {
[K in keyof T]?: T[K];
};6.3 API DTO Pattern
type ApiUser = {
id: string;
name: string;
createdAt: string;
};
type UiUser = {
id: string;
name: string;
createdAt: Date;
};Conversion helper:
function mapUser(apiUser: ApiUser): UiUser {
return {
...apiUser,
createdAt: new Date(apiUser.createdAt),
};
}7. Keyof + Lookup Types (Interview Favorite)
type User = {
id: number;
name: string;
isActive: boolean;
};
type UserValue<T extends keyof User> = User[T];
type NameType = UserValue<"name">; // stringWhy this matters:
- Type-safe property access
- Used in forms, tables, configs
Flowchart: Lookup Type
The constraint T extends keyof User makes an invalid key a compile error.
8. Function Overloads in Real Code
API Helper Example
function fetchData(url: string): Promise<string>;
function fetchData(url: string, parseJson: true): Promise<object>;
function fetchData(url: string, parseJson?: boolean) {
return fetch(url).then((res) => (parseJson ? res.json() : res.text()));
}Used in:
- SDKs
- Utility libraries
- Shared services
9. Type-safe Event Systems
type Events = {
login: { userId: string };
logout: undefined;
};
function emit<K extends keyof Events>(event: K, payload: Events[K]) {}Usage:
emit("login", { userId: "123" });
emit("logout", undefined);Very common in:
- Analytics
- Pub/sub
- Feature flags
10. React-Specific Advanced Patterns
10.1 Generic Components
type ListProps<T> = {
items: T[];
render: (item: T) => React.ReactNode;
};
function List<T>({ items, render }: ListProps<T>) {
return <>{items.map(render)}</>;
}10.2 Polymorphic Components
type ButtonProps<T extends React.ElementType> = {
as?: T;
} & React.ComponentPropsWithoutRef<T>;
function Button<T extends React.ElementType = "button">(props: ButtonProps<T>) {
const { as: Component = "button", ...rest } = props;
return <Component {...rest} />;
}Used in:
- Design systems
- Component libraries
11. API Response Safety Pattern
type ApiSuccess<T> = {
success: true;
data: T;
};
type ApiError = {
success: false;
error: string;
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;Usage:
function handle(res: ApiResponse<User>) {
if (res.success) {
res.data.name;
} else {
res.error;
}
}12. Assertion Functions (asserts keyword)
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value == null) {
throw new Error("Value is null");
}
}Usage:
assertIsDefined(user);
user.name; // fully typedUsed in:
- Runtime validation
- Critical paths
Flowchart: Assertion Function
The compiler assumes the assertion held for every line after the call.
13. Preventing Invalid States (Senior-Level)
Bad
type State = {
loading: boolean;
data?: string[];
error?: string;
};Good
type State =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };This is a huge senior engineering signal.
14. Branded Types (Advanced Safety)
type UserId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}Prevents:
const postId: string = "123";
const userId: UserId = postId; // ❌Used in:
- Finance
- IDs
- Security-sensitive apps
Flowchart: Branded Types
The brand exists only in the type system, so the only way to get a UserId is through the function that creates it.
15. When NOT to Use Advanced Types
Senior rule:
- Avoid clever types if they hurt readability
- Prefer simple interfaces
- Types are for humans first, compiler second
16. What Interviewers Actually Look For
They check if you:
- Model states correctly
- Avoid impossible states
- Use
unknownsafely - Use discriminated unions
- Avoid
any - Understand generics deeply
17. One-Line Senior Summary
Advanced TypeScript is about designing correct systems, not writing complex types.