Back to Blog

TypeScript Generics: Variance, Conditional Types, Template Literals, and Mapped Types

Master advanced TypeScript generics: covariance and contravariance, conditional types with infer, template literal types, mapped type modifiers, and higher-kinded types patterns.

Viprasol Tech Team
September 13, 2026
14 min read

Most TypeScript engineers use generics for simple parameterization: Array<T>, Promise<T>, Record<string, T>. That covers 80% of use cases.

The remaining 20% โ€” the patterns that eliminate repetitive type declarations, catch impossible states at compile time, and make libraries a joy to use โ€” requires understanding variance, conditional types, infer, and mapped type transformations.

This post covers those patterns with real production examples.


Variance: Covariance and Contravariance

Variance describes how subtype relationships work with generic types. It matters when you're writing library types or type-safe higher-order functions.

Covariance (output position)

// Covariant: if Dog extends Animal, then Producer<Dog> extends Producer<Animal>
// Applies to return types โ€” "what comes out"

interface Producer<out T> {  // 'out' marks covariant position
  produce(): T;
}

class DogProducer implements Producer<Dog> {
  produce(): Dog { return new Dog(); }
}

// โœ… This is safe โ€” a DogProducer can stand in for an AnimalProducer
// because everything a DogProducer produces is an Animal
const animalProducer: Producer<Animal> = new DogProducer();

Contravariance (input position)

// Contravariant: if Dog extends Animal, Consumer<Animal> extends Consumer<Dog>
// Applies to parameter types โ€” "what goes in" โ€” direction is reversed!

interface Consumer<in T> {  // 'in' marks contravariant position
  consume(item: T): void;
}

class AnimalConsumer implements Consumer<Animal> {
  consume(animal: Animal): void { /* handle any animal */ }
}

// โœ… This is safe โ€” an AnimalConsumer can stand in for a DogConsumer
// because it can handle Dogs (which are Animals)
const dogConsumer: Consumer<Dog> = new AnimalConsumer();

Why This Matters in Practice

// Function types are contravariant in parameters, covariant in return type
type Callback<T> = (value: T) => void;

// โŒ TypeScript will catch this (function parameter contravariance)
const dogCallback: Callback<Dog> = (dog) => dog.bark();
const animalCallback: Callback<Animal> = dogCallback; // Error! Dog callback can't handle all Animals

// โœ… Correct direction
const animalCallback2: Callback<Animal> = (animal) => animal.breathe();
const dogCallback2: Callback<Dog> = animalCallback2; // OK โ€” animal handler can handle dogs

Conditional Types

Conditional types evaluate to different types based on a condition:

// Basic syntax
type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>;  // true
type B = IsArray<string>;    // false

// Distributive: conditional types distribute over unions
type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>;  // string[] | number[]

// Non-distributive (wrap in tuple to prevent distribution)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type D = ToArrayNonDist<string | number>;  // (string | number)[]

Real-World: Making Props Optional Based on Other Props

// Button can be "link" (needs href) or "button" (needs onClick)
// Making both required would force consumers to always provide both

type ButtonBaseProps = {
  label: string;
  disabled?: boolean;
  className?: string;
};

type ButtonAsButton = ButtonBaseProps & {
  as?: "button";
  onClick: () => void;
  href?: never;  // Prevent href when as="button"
};

type ButtonAsLink = ButtonBaseProps & {
  as: "link";
  href: string;
  onClick?: never;  // Prevent onClick when as="link"
};

type ButtonProps = ButtonAsButton | ButtonAsLink;

// TypeScript enforces correct usage:
// โœ… <Button as="link" label="Go" href="/home" />
// โœ… <Button label="Submit" onClick={handleSubmit} />
// โŒ <Button label="Bad" />  โ€” missing onClick
// โŒ <Button as="link" label="Bad" onClick={fn} />  โ€” href missing, onClick not allowed

๐ŸŒ 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

The infer Keyword

infer captures type information within conditional types:

// Extract the return type of a function
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never;

// Extract the element type from an array
type ArrayElement<T> = T extends (infer E)[] ? E : never;

// Extract the resolved type from a Promise
type Awaited<T> = T extends Promise<infer R>
  ? R extends Promise<any>
    ? Awaited<R>  // Unwrap nested promises
    : R
  : T;

// Extract function parameter types
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

Production Use: Type-Safe Event System

// Infer payload type from event name
interface EventMap {
  "user.created": { userId: string; email: string };
  "order.completed": { orderId: string; totalCents: number; items: string[] };
  "payment.failed": { paymentIntentId: string; reason: string };
}

type EventPayload<E extends keyof EventMap> = EventMap[E];

// Type-safe event emitter
class TypedEventEmitter {
  private handlers: Map<string, Set<(payload: unknown) => void>> = new Map();

  on<E extends keyof EventMap>(
    event: E,
    handler: (payload: EventPayload<E>) => void
  ): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler as (payload: unknown) => void);

    // Return unsubscribe function
    return () => this.handlers.get(event)?.delete(handler as (payload: unknown) => void);
  }

  emit<E extends keyof EventMap>(event: E, payload: EventPayload<E>): void {
    this.handlers.get(event)?.forEach((handler) => handler(payload));
  }
}

const emitter = new TypedEventEmitter();

// โœ… TypeScript knows payload.userId is string
emitter.on("user.created", (payload) => {
  console.log(payload.userId); // string
  console.log(payload.email);  // string
});

// โœ… TypeScript enforces correct payload shape
emitter.emit("user.created", { userId: "u-123", email: "test@example.com" });

// โŒ TypeScript catches wrong event/payload combinations
emitter.emit("user.created", { orderId: "o-123" }); // Error!

Mapped Types

Mapped types transform existing types by iterating over their keys:

// Built-in mapped types (simplified implementations)
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 optionality
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// Mapping with modifier:
// + adds, - removes, readonly and ? are modifiers
type Mutable<T> = { -readonly [K in keyof T]: T[K] };  // Remove readonly
type NonNullable<T> = { [K in keyof T]-?: NonNullable<T[K]> };

Deep Partial / Deep Readonly

// Recursively make all properties partial
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

// Recursively make all properties readonly
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

// Usage
interface Config {
  server: {
    host: string;
    port: number;
    tls: { cert: string; key: string };
  };
  db: { url: string; poolSize: number };
}

type PartialConfig = DeepPartial<Config>;
// { server?: { host?: string; port?: number; tls?: { cert?: string; key?: string } }; db?: ... }

const defaultConfig: DeepReadonly<Config> = {
  server: { host: "localhost", port: 3000, tls: { cert: "", key: "" } },
  db: { url: "postgres://localhost", poolSize: 10 },
};

// โŒ TypeScript prevents mutation of deeply readonly
// defaultConfig.server.port = 8080;  // Error!

Mapped Types with as Re-mapping

// Transform property names using template literals + as
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
}

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

// Filter properties by value type
type StringProperties<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type UserStringProps = StringProperties<User>;
// { name: string; email: string }  โ€” age excluded (number)

// Create event handler types from entity types
type EventHandlers<T, Prefix extends string = "on"> = {
  [K in keyof T as `${Prefix}${Capitalize<string & K>}Change`]?: (
    oldValue: T[K],
    newValue: T[K]
  ) => void;
};

type UserEventHandlers = EventHandlers<User>;
// {
//   onNameChange?: (old: string, new: string) => void;
//   onAgeChange?: (old: number, new: number) => void;
//   onEmailChange?: (old: string, new: string) => void;
// }

๐Ÿš€ 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

Template Literal Types

// Route parameter extraction
type ExtractRouteParams<Route extends string> =
  Route extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<`/${Rest}`>
    : Route extends `${string}:${infer Param}`
    ? Param
    : never;

type UserRouteParams = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

// Type-safe route builder
function buildRoute<Route extends string>(
  route: Route,
  params: Record<ExtractRouteParams<Route>, string>
): string {
  let result: string = route;
  for (const [key, value] of Object.entries(params)) {
    result = result.replace(`:${key}`, value as string);
  }
  return result;
}

// โœ… TypeScript enforces all params are provided
const url = buildRoute("/users/:userId/posts/:postId", {
  userId: "u-123",
  postId: "p-456",
});

// โŒ Missing postId โ€” TypeScript error
// buildRoute("/users/:userId/posts/:postId", { userId: "u-123" });

CSS-in-TypeScript Autocomplete

// Type-safe CSS class builder with Tailwind-like syntax
type SpacingScale = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24;
type ColorScale = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
type ColorName = "red" | "blue" | "green" | "yellow" | "gray" | "purple";

type TextColor = `text-${ColorName}-${ColorScale}`;
type BgColor = `bg-${ColorName}-${ColorScale}`;
type Padding = `p-${SpacingScale}` | `px-${SpacingScale}` | `py-${SpacingScale}`;
type Margin = `m-${SpacingScale}` | `mx-${SpacingScale}` | `my-${SpacingScale}`;

type TailwindClass = TextColor | BgColor | Padding | Margin | "flex" | "grid" | "hidden";

function cn(...classes: (TailwindClass | undefined | false | null)[]): string {
  return classes.filter(Boolean).join(" ");
}

// โœ… Full autocomplete โ€” TypeScript knows all valid classes
const buttonClass = cn("bg-blue-500", "text-white-100", "px-4", "py-2");
// โŒ TypeScript catches invalid classes
// cn("bg-invalid-999");  // Error!

Higher-Kinded Types via Type Constructor Pattern

TypeScript doesn't support higher-kinded types natively, but you can emulate them:

// Type constructor registry pattern
interface TypeConstructors {
  Array: { Kind: unknown[]; new<T>(): T[] };
  Promise: { Kind: Promise<unknown>; new<T>(): Promise<T> };
  Option: { Kind: Option<unknown>; new<T>(): Option<T> };
}

type HKT = keyof TypeConstructors;

// Apply a type constructor to a type argument
type Apply<F extends HKT, A> = TypeConstructors[F]["Kind"] extends infer R
  ? R extends { _phantom: infer _ }
    ? Omit<R, "_phantom"> & { _phantom: A }
    : never
  : never;

// Functor typeclass โ€” any type that can be mapped over
interface Functor<F extends HKT> {
  map<A, B>(fa: Apply<F, A>, f: (a: A) => B): Apply<F, B>;
}

// This enables writing generic code that works on any "container" type
// (Array, Promise, Option, Either, Observable)

Utility Type Library

// src/types/utilities.ts โ€” reusable type utilities

/** Make specific keys optional */
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

/** Make specific keys required */
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

/** Recursively flatten union types */
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends
  (k: infer I) => void
  ? I
  : never;

/** Get all keys where value matches a type */
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

/** Exact object type โ€” prevents extra properties */
type Exact<T, U extends T = T> = T & Record<Exclude<keyof U, keyof T>, never>;

/** Nominal typing โ€” create branded types */
type Brand<T, B extends string> = T & { readonly _brand: B };

type UserId = Brand<string, "UserId">;
type TenantId = Brand<string, "TenantId">;
type OrderId = Brand<string, "OrderId">;

// Usage: prevents mixing up ID types
function getUser(id: UserId): Promise<User> { /* ... */ return Promise.resolve({} as User); }

const userId = "u-123" as UserId;
const tenantId = "t-456" as TenantId;

getUser(userId);   // โœ…
// getUser(tenantId); // โŒ Error: TenantId is not UserId

/** Tuple of length N */
type Tuple<T, N extends number, R extends T[] = []> = 
  R["length"] extends N ? R : Tuple<T, N, [T, ...R]>;

type Triple<T> = Tuple<T, 3>;
const rgb: Triple<number> = [255, 128, 0]; // โœ… Exactly 3 numbers

See Also


Working With Viprasol

Strong TypeScript types eliminate entire categories of runtime bugs and make large codebases refactorable. Our engineers design type systems that catch impossible states at compile time, from database query results to API response shapes to state machine transitions.

Web development services โ†’ | Start a project โ†’

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.