Back to Blog

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.

Viprasol Tech Team
April 5, 2027
14 min read

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:

AspectImpact
TypeScript compilation speedComplex conditional/recursive types slow the compiler
Team onboardingAdvanced types are hard to read without experience
Error messagesComplex generics produce terrifying error messages
MaintenanceTypes 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-ignore is creeping in because the types are fighting you
  • When a union type would be clearer than a conditional type

See Also


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.

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.