Advanced TypeScript Patterns
Knowing interface vs type is table stakes. Senior frontend interviews test whether you can model complex domains with the type system â and whether you know when not to.
Generics: Parameterized Types
Generics let you write functions and types that work with any type while preserving type information:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // number
first(['a', 'b', 'c']); // stringConstrained Generics
Restrict what types are accepted with extends:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alex', age: 30 };
getProperty(user, 'name'); // string
getProperty(user, 'foo'); // Error: '"foo"' is not assignable to keyof typeof userGeneric Components in React
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map((item) => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}
// Usage: T is inferred as User
<List items={users} renderItem={(u) => u.name} keyExtractor={(u) => u.id} />Conditional Types
Types that depend on a condition â TypeScript's ternary operator at the type level:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // falseDistributive Conditional Types
When T is a union, conditional types distribute across each member:
type Extract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number>;
// number â only the member that extends number survivesinfer: Pattern Matching for Types
Extract types from complex structures:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
type A = ReturnType<() => string>; // string
type B = PromiseValue<Promise<number>>; // number
type C = PromiseValue<string>; // string (passthrough)Mapped Types
Transform existing types by iterating over their keys:
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };Key Remapping (TypeScript 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }Filtering Keys
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type StringProps = PickByType<{ name: string; age: number; active: boolean }, string>;
// { name: string }Template Literal Types
String manipulation at the type level:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type CSSProperty = 'margin' | 'padding';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// 'margin-top' | 'margin-right' | ... | 'padding-left' (8 combinations)Practical: Type-Safe Event Emitter
type EventMap = {
click: { x: number; y: number };
keydown: { key: string };
};
type EventHandler<T extends keyof EventMap> = (payload: EventMap[T]) => void;
class Emitter<E extends Record<string, any>> {
private handlers = new Map<string, Function[]>();
on<K extends keyof E & string>(event: K, handler: (payload: E[K]) => void) {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit<K extends keyof E & string>(event: K, payload: E[K]) {
this.handlers.get(event)?.forEach((fn) => fn(payload));
}
}
const bus = new Emitter<EventMap>();
bus.on('click', ({ x, y }) => {}); // fully typed
bus.emit('keydown', { key: 'Enter' }); // fully typedDiscriminated Unions
The single most useful pattern for modeling state:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function render(state: AsyncState<User>) {
switch (state.status) {
case 'idle': return null;
case 'loading': return <Spinner />;
case 'success': return <Profile user={state.data} />; // data is narrowed to User
case 'error': return <ErrorMessage error={state.error} />;
}
}The status field is the discriminant â TypeScript narrows the union based on its value, making illegal states unrepresentable.
Interview Signal
Advanced TypeScript questions test:
- Modeling skill â can you represent business rules as types that prevent invalid states?
- Utility type fluency â knowing
Pick,Omit,Extract,Exclude,ReturnType, and when to compose them - Restraint â knowing when a simple
Recordor union is better than a 10-line conditional mapped generic type - Practical application â generics in React components, discriminated unions for state machines, template literals for API contracts