Why Advanced Generics Matter
Basic generics let you write reusable functions. Advanced generics let you write self-documenting APIs where the type system prevents entire classes of bugs at compile time — no runtime checks needed. Once you internalize conditional types and mapped types, you stop writing defensive runtime code and start encoding invariants directly in your type signatures.
This guide moves fast. Each section has a concrete, production-relevant example you can adapt immediately. Use the DevKits JSON Formatter to validate any JSON payloads you type-check, and the TypeScript Formatter to clean up your type definitions.
Generic Constraints and Default Types
The foundation before going advanced: constraints narrow what a type parameter accepts, while defaults make APIs ergonomic.
// Constraint: T must have an id property
function findById<T extends { id: string | number }>(
items: T[],
id: T["id"]
): T | undefined {
return items.find(item => item.id === id);
}
// Default type parameter (TypeScript 4.1+)
type ApiResponse<T = unknown> = {
data: T;
status: number;
message: string;
};
// Without a type arg it defaults to unknown — forces explicit narrowing
const raw: ApiResponse = await fetch("/api/user").then(r => r.json());
const typed: ApiResponse<User> = await fetchUser(1);
Conditional Types
Conditional types follow the T extends U ? X : Y syntax — essentially a ternary operator at the type level.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical: flatten arrays one level deep
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number (not an array, returned as-is)
// Distributive conditional types — applies to each union member
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArr = ToArray<string | number>;
// string[] | number[] (NOT (string | number)[])
// Prevent distribution by wrapping in a tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Combined = ToArrayNonDist<string | number>;
// (string | number)[]
The infer Keyword
infer is only valid inside conditional types. It extracts a type from a structural position and binds it to a new type variable. This is how TypeScript's built-in ReturnType, Parameters, and Awaited are implemented.
// Extract the return type of any function
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
// Extract the first argument type
type FirstArg<T extends (...args: any) => any> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
function greet(name: string, age: number): string {
return `Hello ${name}`;
}
type GreetReturn = ReturnType<typeof greet>; // string
type GreetFirst = FirstArg<typeof greet>; // string
// Unwrap Promises — this is essentially what Awaited<T> does
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Resolved = Unwrap<Promise<Promise<number>>>; // number
// Extract a tuple element at a specific index
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never;
type H = Head<[string, number, boolean]>; // string
type T = Tail<[string, number, boolean]>; // [number, boolean]
Mapped Types
Mapped types iterate over the keys of an existing type and transform them. Combined with modifiers (+/- for readonly and ?), they are extremely powerful.
// All properties optional (like Partial<T>)
type Optional<T> = { [K in keyof T]?: T[K] };
// Remove readonly — useful for tests that mutate fixtures
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// Make specific keys required, rest optional
type RequireKeys<T, K extends keyof T> =
Omit<T, K> & Required<Pick<T, K>>;
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// host and port are required, debug remains optional
type ServerConfig = RequireKeys<Config, "host" | "port">;
// Remap keys with "as" clause (TypeScript 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
// Filter keys by value type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K]
};
interface Mixed {
name: string;
count: number;
active: boolean;
label: string;
}
type StringKeys = PickByValue<Mixed, string>;
// { name: string; label: string }
Template Literal Types
Template literal types (TypeScript 4.1+) bring string manipulation to the type level. Essential for type-safe event emitters, CSS-in-JS, and API route definitions.
type EventName = "click" | "focus" | "blur";
// Generate "onClick" | "onFocus" | "onBlur"
type Handler = `on${Capitalize<EventName>}`;
// Type-safe CSS property builder
type CSSProperty = "margin" | "padding" | "border";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSLonghand = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "border-left" (12 combinations)
// Parse URL params at type level
type ExtractRouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
function route<T extends string>(
path: T,
params: Record<ExtractRouteParams<T>, string | number>
): string {
return path.replace(/:([a-z]+)/gi, (_, key) => String(params[key as keyof typeof params]));
}
route("/users/:userId/posts/:postId", { userId: 42, postId: 7 }); // OK
// route("/users/:userId", { id: 42 }); // Error: 'id' not assignable
Common Pitfalls
Accidentally widening with conditional types
When TypeScript cannot resolve a conditional type at definition time (because the type parameter is still generic), it defers evaluation. This can surprise you:
type IsNever<T> = T extends never ? true : false;
// This returns boolean, not false!
type Test = IsNever<string>; // boolean (deferred!)
// Fix: wrap in a non-distributive form
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type TestFixed = IsNeverFixed<string>; // false ✓
Forgetting to handle undefined in mapped types
// Dangerous: deep access can be undefined
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Better: explicit undefined handling for primitives
type SafeDeepPartial<T> = T extends object
? { [K in keyof T]?: SafeDeepPartial<T[K]> }
: T | undefined;
Excessive type instantiation depth
Recursive types can hit TypeScript's instantiation depth limit (default ~100). If you see "Type instantiation is excessively deep", add a base case or use // @ts-expect-error strategically. Alternatively, consider a branded type approach instead of structural recursion.
Practical Utility: Type-Safe Event Emitter
type EventMap = Record<string, any>;
class TypedEmitter<Events extends EventMap> {
private handlers: Partial<{ [K in keyof Events]: Array<(data: Events[K]) => void> }> = {};
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): this {
const list = (this.handlers[event] ??= []);
list.push(handler);
return this;
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.handlers[event]?.forEach(h => h(data));
}
}
// Usage
interface AppEvents {
userLoggedIn: { userId: string; timestamp: number };
orderPlaced: { orderId: string; total: number };
}
const emitter = new TypedEmitter<AppEvents>();
emitter.on("userLoggedIn", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.emit("userLoggedIn", { userId: "u_123", timestamp: Date.now() }); // OK
// emitter.emit("userLoggedIn", { userId: 42 }); // Error: number not assignable to string
You can validate your TypeScript type structures by pasting the compiled JSON output into the DevKits JSON Formatter or using the Regex Tester to verify string pattern types at runtime.
Summary
- Use generic constraints to narrow acceptable types and improve IDE autocomplete
- Use conditional types to branch on structural compatibility — wrap in tuples to prevent unintended distribution
- infer extracts type fragments from positions; combine with recursion for deep unwrapping
- Mapped types with
asclauses and-readonly/-?modifiers cover most transformation needs - Template literal types enable type-safe string APIs without any runtime overhead
Advanced TypeScript types are a force multiplier: invest a few hours in understanding them and your entire team benefits from APIs that are self-documenting and impossible to misuse.