TypeScript Template Literal Types in 2026: infer, Mapped Types, and Conditional Types
Master TypeScript template literal types: string interpolation in types, infer keyword, mapped types with remapping, conditional types, and building type-safe APIs and event systems.
TypeScript Template Literal Types in 2026: infer, Mapped Types, and Conditional Types
TypeScript's type system became Turing-complete around v4.1 when template literal types landed. Combined with infer, mapped types with as remapping, and conditional types, you can compute types that adapt to your data at compile time โ no runtime overhead.
This post covers practical applications: type-safe event emitters with on${Capitalize<Event>} handlers, API route type inference, CSS-in-TS property validation, ORM field selectors, and builder pattern types.
Template Literal Types: The Basics
// String interpolation in types
type Direction = "left" | "right" | "up" | "down";
type PaddingKey = `padding-${Direction}`;
// "padding-left" | "padding-right" | "padding-up" | "padding-down"
// Cross product of two unions
type Side = "top" | "bottom";
type Axis = "x" | "y";
type MotionClass = `motion-${Side}-${Axis}`;
// "motion-top-x" | "motion-top-y" | "motion-bottom-x" | "motion-bottom-y"
// Intrinsic string manipulation types (built into TypeScript)
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type Getter<T extends string> = `get${Capitalize<T>}`;
type Setter<T extends string> = `set${Capitalize<T>}`;
type BooleanGetter<T extends string> = `is${Capitalize<T>}` | `has${Capitalize<T>}`;
type GetUser = Getter<"user">; // "getUser"
type SetUser = Setter<"user">; // "setUser"
Type-Safe Event Emitter
// A typed event emitter where listener names derive from event names
type EventMap = {
"user.created": { userId: string; email: string };
"order.placed": { orderId: string; total: number };
"payment.failed": { invoiceId: string; reason: string };
"session.expired": { sessionId: string };
};
// Convert "user.created" โ "userCreated" (dot to camelCase)
type DotToCamel<S extends string> =
S extends `${infer Head}.${infer Tail}`
? `${Head}${Capitalize<DotToCamel<Tail>>}`
: S;
type Test1 = DotToCamel<"user.created">; // "userCreated"
type Test2 = DotToCamel<"order.placed">; // "orderPlaced"
// Build listener method names: "onUserCreated", "onOrderPlaced"
type ListenerMethods = {
[K in keyof EventMap as `on${Capitalize<DotToCamel<K & string>>}`]:
(payload: EventMap[K]) => void;
};
// Result type:
// {
// onUserCreated: (payload: { userId: string; email: string }) => void;
// onOrderPlaced: (payload: { orderId: string; total: number }) => void;
// onPaymentFailed: (payload: { invoiceId: string; reason: string }) => void;
// onSessionExpired: (payload: { sessionId: string }) => void;
// }
class TypedEmitter {
private listeners = new Map<string, Set<Function>>();
on<K extends keyof EventMap>(
event: K,
listener: (payload: EventMap[K]) => void
): this {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event)!.add(listener);
return this;
}
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
this.listeners.get(event)?.forEach((listener) => listener(payload));
}
}
const emitter = new TypedEmitter();
// โ
Correct: TypeScript knows the payload shape for each event
emitter.on("user.created", ({ userId, email }) => {
console.log(`New user ${userId}: ${email}`);
});
// โ TypeScript error: 'total' does not exist on user.created payload
emitter.on("user.created", ({ total }) => {}); // Error!
๐ 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
infer: Extracting Types from Positions
// infer extracts a type from a specific position in a conditional type
// Extract return type (same as built-in ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract first argument type
type FirstArg<T> = T extends (first: infer A, ...rest: any[]) => any ? A : never;
type F = FirstArg<(x: string, y: number) => boolean>; // string
// Extract Promise value type (same as Awaited<T>)
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type U = Unwrap<Promise<{ id: string }>>; // { id: string }
// Extract array element type
type ElementOf<T> = T extends Array<infer E> ? E : never;
type E = ElementOf<string[]>; // string
// Extract deeply nested types
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U> // Recursive: unwrap nested promises
: T;
type Triple = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>; // string
Extracting from Template Literal Patterns
// Parse route parameters from URL patterns
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params1 = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
type Params2 = ExtractRouteParams<"/workspaces/:workspaceId/members/:memberId/roles">;
// "workspaceId" | "memberId"
// Build params object type from route string
type RouteParams<T extends string> = {
[K in ExtractRouteParams<T>]: string;
};
type UserPostParams = RouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
// Type-safe route builder
function buildRoute<T extends string>(
template: T,
params: RouteParams<T>
): string {
return Object.entries(params).reduce(
(acc, [key, val]) => acc.replace(`:${key}`, val as string),
template as string
);
}
// โ
TypeScript enforces all params are provided
const url = buildRoute("/users/:userId/posts/:postId", {
userId: "abc",
postId: "123",
});
// โ TypeScript error: missing postId
const bad = buildRoute("/users/:userId/posts/:postId", { userId: "abc" });
๐ 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
Mapped Types with as Remapping
// Remap keys using template literals
// Getters and setters from a config type
type Config = {
theme: "light" | "dark";
language: string;
fontSize: number;
};
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
};
type ConfigAPI = Getters<Config> & Setters<Config>;
// {
// getTheme: () => "light" | "dark";
// getLanguage: () => string;
// getFontSize: () => number;
// setTheme: (value: "light" | "dark") => void;
// setLanguage: (value: string) => void;
// setFontSize: (value: number) => void;
// }
// Filter out keys (remap to never removes them)
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringOnlyConfig = OnlyStrings<Config>;
// { theme: "light" | "dark"; language: string; }
// fontSize removed because it's a number
// Prefix all keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<K & string>}`]: T[K];
};
type DbRow = Prefixed<Config, "db_">;
// { db_Theme: "light" | "dark"; db_Language: string; db_FontSize: number }
Type-Safe ORM Field Selector
// Type-safe Prisma-like select objects
type User = {
id: string;
email: string;
name: string;
passwordHash: string;
createdAt: Date;
};
// Select<T>: maps each key to boolean (include or exclude)
type Select<T> = {
[K in keyof T]?: boolean;
};
// SelectedResult<T, S>: only keys where S[K] = true
type SelectedResult<T, S extends Select<T>> = {
[K in keyof T as S[K] extends true ? K : never]: T[K];
};
// Type-safe select function
function selectFields<T, S extends Select<T>>(
entity: T,
select: S
): SelectedResult<T, S> {
return Object.fromEntries(
Object.entries(entity as object)
.filter(([key]) => select[key as keyof T] === true)
) as SelectedResult<T, S>;
}
const user: User = {
id: "123", email: "user@example.com", name: "Alice",
passwordHash: "hashed", createdAt: new Date(),
};
// โ
TypeScript knows exactly which fields are returned
const safeUser = selectFields(user, { id: true, email: true, name: true });
// Type: { id: string; email: string; name: string }
// โ TypeScript error: passwordHash not in the selected type
console.log(safeUser.passwordHash); // Error: Property 'passwordHash' does not exist
Conditional Types: Distributive Behavior
// Conditional types distribute over unions
type IsArray<T> = T extends any[] ? "array" : "not array";
type Test1 = IsArray<string[]>; // "array"
type Test2 = IsArray<string>; // "not array"
type Test3 = IsArray<string | number[]>; // "not array" | "array" (distributes!)
// Use [T] to prevent distribution
type IsArrayStrict<T> = [T] extends [any[]] ? "array" : "not array";
type Test4 = IsArrayStrict<string | number[]>; // "not array" (union not distributed)
// Practical: exclude undefined/null from a type
type NonNullable<T> = T extends null | undefined ? never : T;
// Same as built-in NonNullable<T>
// Deep required (remove all optional modifiers recursively)
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
type PartialUser = { name?: string; address?: { city?: string; zip?: string } };
type FullUser = DeepRequired<PartialUser>;
// { name: string; address: { city: string; zip: string } }
Building a Type-Safe Event Bus
// Full example: typed event bus with wildcard subscription
type EventKey<T extends Record<string, unknown>> = keyof T & string;
type EventHandler<T> = (payload: T) => void | Promise<void>;
class TypedEventBus<Events extends Record<string, unknown>> {
private handlers = new Map<string, Set<EventHandler<unknown>>>();
// Subscribe to specific event
subscribe<K extends EventKey<Events>>(
event: K,
handler: EventHandler<Events[K]>
): () => void {
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
this.handlers.get(event)!.add(handler as EventHandler<unknown>);
// Return unsubscribe function
return () => this.handlers.get(event)?.delete(handler as EventHandler<unknown>);
}
// Subscribe to all events matching a pattern
subscribePattern<K extends EventKey<Events>>(
pattern: `${string}.*`,
handler: (event: K, payload: Events[K]) => void
): () => void {
const prefix = pattern.replace(".*", "");
const wrapper = (event: string, payload: unknown) => {
if (event.startsWith(prefix)) handler(event as K, payload as Events[K]);
};
// ... implementation
return () => {};
}
async publish<K extends EventKey<Events>>(
event: K,
payload: Events[K]
): Promise<void> {
const handlers = this.handlers.get(event);
if (!handlers) return;
await Promise.allSettled([...handlers].map((h) => h(payload)));
}
}
// Usage:
const bus = new TypedEventBus<EventMap>();
const unsubscribe = bus.subscribe("user.created", ({ userId, email }) => {
console.log(`${userId}: ${email}`);
});
await bus.publish("user.created", { userId: "123", email: "a@b.com" });
// โ
TypeScript enforces payload shape
unsubscribe(); // Clean up
Cost and Timeline
| Task | Timeline | Cost (USD) |
|---|---|---|
| Type utility library design | 0.5โ1 day | $400โ$800 |
| Route params type extraction | 0.5 day | $300โ$500 |
| Typed event emitter system | 0.5โ1 day | $400โ$800 |
| ORM-style select types | 0.5 day | $300โ$500 |
| Full advanced type system audit | 1โ2 weeks | $5,000โ$10,000 |
See Also
- TypeScript Utility Types โ Built-in utilities and branded types
- TypeScript Decorators โ Metadata + type-safe decorators
- TypeScript Error Handling โ Typed error unions
- React State Machines โ Using discriminated unions for state
Working With Viprasol
We design TypeScript type systems for SaaS products โ from basic utility types through complex template literal type extraction and type-level computation. Our team has built type-safe event buses, route parameter extractors, and ORM-style query builders that catch entire categories of bugs at compile time.
What we deliver:
- Template literal type utilities for your domain (event names, route params, CSS keys)
- Type-safe event emitter with inferred payload types
- Mapped type transformations for getter/setter generation
- Conditional type utilities (DeepRequired, NonNullable, DeepPartial)
- TypeScript strict mode migration and type coverage analysis
Explore our web development services or contact us to strengthen your TypeScript type system.
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.