TypeScript Advanced Generics: Conditional Types, infer, Mapped Types, and Template Literals in Practice
Master TypeScript advanced generics. Covers conditional types with infer, mapped types with key remapping, template literal types for API contracts, recursive types, distributive conditionals, and real-world utility type patterns.
Most TypeScript developers are comfortable with basic generics โ Array<T>, Promise<T>, generic functions. Advanced generics โ conditional types, infer, mapped types with key remapping, and template literal types โ unlock a different level of type safety where the type system itself encodes your API contracts, derives types from runtime shapes, and catches entire categories of bugs at compile time.
This guide is about the patterns that actually appear in production TypeScript and the reasoning behind them.
Conditional Types
Conditional types branch on type relationships, like ternary expressions for types:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
// Practical: extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => { id: string; name: string };
type FnReturn = ReturnType<Fn>; // { id: string; name: string }
Distributive behavior: When a conditional type is applied to a union, it distributes:
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumberArray = ToArray<string | number>;
// Distributes: (string extends any ? string[] : never) | (number extends any ? number[] : never)
// Result: string[] | number[]
To prevent distribution, wrap in a tuple:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>; // (string | number)[] โ not distributed
The infer Keyword
infer captures a type from within a conditional type:
// Extract element type from array
type ElementType<T> = T extends Array<infer E> ? E : never;
type Elem = ElementType<string[]>; // string
// Extract Promise resolved type
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type Val = Awaited<Promise<Promise<number>>>; // number
// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type P = Parameters<(a: string, b: number) => void>; // [string, number]
// Extract the first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F = FirstParam<(id: string, name: string) => void>; // string
Real-World: Extract Event Payload Type
// Pattern: typed event bus where payload types are inferred per event name
type EventMap = {
"user.created": { id: string; email: string; name: string };
"order.placed": { orderId: string; total: number; currency: string };
"payment.failed": { invoiceId: string; reason: string };
};
type EventPayload<T extends keyof EventMap> = EventMap[T];
// Handler type: strongly typed per event
type Handler<T extends keyof EventMap> = (payload: EventPayload<T>) => void | Promise<void>;
// Usage โ TypeScript infers payload type from event name
function on<T extends keyof EventMap>(event: T, handler: Handler<T>): void {
// implementation
}
on("user.created", (payload) => {
// payload: { id: string; email: string; name: string } โ fully typed
console.log(payload.email);
});
on("order.placed", (payload) => {
// payload: { orderId: string; total: number; currency: string }
console.log(payload.total.toFixed(2));
});
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Mapped Types
Mapped types transform the properties of an existing type:
// Built-in equivalents โ understand them, don't just use them
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] }; // -? removes optional
type Record<K extends keyof any, V> = { [P in K]: V };
Key Remapping with as
// Prefix all keys with "get"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type User = { id: string; name: string; email: string };
type UserGetters = Getters<User>;
// {
// getId: () => string;
// getName: () => string;
// getEmail: () => string;
// }
Filter Keys by Value Type
// Keep only properties whose values are a certain type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
type Config = {
apiKey: string;
retries: number;
timeout: number;
debug: boolean;
};
type NumericConfig = PickByValue<Config, number>;
// { retries: number; timeout: number }
type StringConfig = PickByValue<Config, string>;
// { apiKey: string }
Deep Partial (Recursive Mapped Type)
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type Config = {
database: {
host: string;
port: number;
credentials: { username: string; password: string };
};
cache: { ttl: number; maxSize: number };
};
type PartialConfig = DeepPartial<Config>;
// All nested properties optional โ useful for config merging
function mergeConfig(defaults: Config, overrides: PartialConfig): Config {
return deepMerge(defaults, overrides);
}
Template Literal Types
Template literal types combine string literals at the type level:
// Build event names from entity + action
type Entity = "user" | "order" | "invoice";
type Action = "created" | "updated" | "deleted";
type EventName = `${Entity}.${Action}`;
// "user.created" | "user.updated" | "user.deleted" |
// "order.created" | ...
// CSS property โ value type mapping
type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSLonghand = `${CSSProperty}-${Direction}`;
// "margin-top" | "margin-right" | ... | "padding-left"
// API route types
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type ApiVersion = "v1" | "v2";
type ApiRoute = `/${ApiVersion}/${string}`;
Extract Route Parameters
// Parse URL parameters from path string at type level
type ExtractRouteParams<Path extends string> =
Path extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: Path extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
// Create route handler with typed params
function createRoute<Path extends string>(
path: Path,
handler: (params: Record<ExtractRouteParams<Path>, string>) => Response
) {
// implementation
}
createRoute("/users/:userId/posts/:postId", (params) => {
// params.userId: string โ
// params.postId: string โ
// params.invalid: Error โ
return new Response(params.userId);
});
Typed Event Emitter with Template Literals
type EventName = `${string}:${string}`;
type EventHandlers<Events extends Record<string, unknown>> = {
[K in keyof Events]: (payload: Events[K]) => void;
};
class TypedEventEmitter<Events extends Record<EventName, unknown>> {
private handlers = new Map<string, Set<Function>>();
on<K extends keyof Events>(
event: K,
handler: (payload: Events[K]) => void
): () => void {
if (!this.handlers.has(event as string)) {
this.handlers.set(event as string, new Set());
}
this.handlers.get(event as string)!.add(handler);
return () => this.handlers.get(event as string)?.delete(handler);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.handlers.get(event as string)?.forEach((h) => h(payload));
}
}
// Usage
type AppEvents = {
"user:created": { id: string; email: string };
"order:placed": { orderId: string; total: number };
"payment:failed": { invoiceId: string };
};
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on("user:created", (p) => console.log(p.email)); // p fully typed
emitter.emit("user:created", { id: "1", email: "a@b.com" }); // payload typed
// emitter.emit("user:created", { id: "1" }); // Error: missing email โ
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Recursive Types
// JSON value type (recursive)
type JSONPrimitive = string | number | boolean | null;
type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue };
// Deep readonly (recursive)
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Config = {
server: { port: number; host: string };
features: string[];
};
const config: DeepReadonly<Config> = {
server: { port: 3000, host: "localhost" },
features: ["auth", "payments"],
};
// config.server.port = 4000; // Error โ
// config.features.push("email"); // Error โ
Practical: Type-Safe Builder Pattern
// Query builder with accumulated type state
type QueryState = {
table: string;
selected: string[];
where: string[];
limit: number | null;
};
class QueryBuilder<State extends Partial<QueryState> = {}> {
private state: State;
constructor(state: State = {} as State) {
this.state = state;
}
from<T extends string>(
table: T
): QueryBuilder<State & { table: T }> {
return new QueryBuilder({ ...this.state, table });
}
select<T extends string[]>(
...cols: T
): QueryBuilder<State & { selected: T }> {
return new QueryBuilder({ ...this.state, selected: cols });
}
where(condition: string): QueryBuilder<State & { where: string[] }> {
return new QueryBuilder({
...this.state,
where: [...(this.state.where ?? []), condition],
});
}
// build() only available when table is set
build(
this: QueryBuilder<State & { table: string }>
): string {
const cols = (this.state as any).selected?.join(", ") ?? "*";
const wheres = (this.state as any).where;
const where = wheres?.length ? `WHERE ${wheres.join(" AND ")}` : "";
return `SELECT ${cols} FROM ${(this.state as any).table} ${where}`.trim();
}
}
const query = new QueryBuilder()
.from("users")
.select("id", "name", "email")
.where("active = true")
.build();
// "SELECT id, name, email FROM users WHERE active = true"
// new QueryBuilder().build(); // Error: build not available without .from() โ
Practical: Prisma-Style Select Types
// Simulate Prisma's typed select โ only return requested fields
type Select<T, S extends { [K in keyof T]?: true }> = {
[K in keyof S as S[K] extends true ? K : never]: K extends keyof T ? T[K] : never;
};
type User = {
id: string;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
};
type UserSelect = Select<User, { id: true; name: true; email: true }>;
// { id: string; name: string; email: string }
// passwordHash excluded โ not in select โ
function findUser<S extends { [K in keyof User]?: true }>(
id: string,
select: S
): Promise<Select<User, S>> {
// implementation
return {} as any;
}
const user = await findUser("1", { id: true, name: true });
// user.id: string โ
// user.name: string โ
// user.email โ Error: not in select โ
Cost of Advanced Types
Advanced TypeScript has a real cost:
| Aspect | Impact |
|---|---|
| TypeScript compilation speed | Complex conditional/recursive types slow the compiler |
| Team onboarding | Advanced types are hard to read without experience |
| Error messages | Complex generics produce terrifying error messages |
| Maintenance | Types can become load-bearing infrastructure that's hard to change |
When to use advanced generics:
- Library/shared code used across many call sites
- API contracts where type safety catches real bugs
- Builder patterns where invalid states should be compile errors
When to use simpler types:
- Application code that changes frequently
- When
// @ts-ignoreis creeping in because the types are fighting you - When a union type would be clearer than a conditional type
See Also
- TypeScript Template Literal Types in Depth
- TypeScript Utility Types for React
- TypeScript Decorators and Metadata
- TypeScript Error Handling Patterns
- React Design System with TypeScript
Working With Viprasol
Advanced TypeScript generics pay off most in shared infrastructure code โ API clients, database query layers, event systems, and component libraries where type safety prevents bugs across hundreds of call sites. Our team uses conditional types and mapped types where they genuinely reduce bugs, and simpler types everywhere else.
What we deliver:
- Type-safe API contracts with template literal and conditional types
- Event system types with payload inference per event name
- Builder pattern types where invalid states are compile errors
- Deep utility types (DeepPartial, DeepReadonly, PickByValue)
- TypeScript configuration for strict mode and project references
Talk to our team about TypeScript architecture โ
Or explore our web development services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.