Back to Blog

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.

Viprasol Tech Team
February 24, 2027
13 min read

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

TaskTimelineCost (USD)
Type utility library design0.5โ€“1 day$400โ€“$800
Route params type extraction0.5 day$300โ€“$500
Typed event emitter system0.5โ€“1 day$400โ€“$800
ORM-style select types0.5 day$300โ€“$500
Full advanced type system audit1โ€“2 weeks$5,000โ€“$10,000

See Also


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.

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.