Advanced Generics and Conditional Types

Generic constraints with keyof, conditional types, the infer keyword, and mapped types with a real validation example.

Beyond basic generics

The advanced-types page in this track introduced generics and a simple constraint (<T extends HasLength>). This page goes further into the type-level programming TypeScript enables: multiple generic constraints working together, types that branch on a condition (conditional types), and types that transform every property of another type (mapped types) — the machinery behind utility types like Partial<T> and Readonly<T> that you've already used as a consumer.

Generic constraints, revisited

A constraint restricts what a generic type parameter is allowed to be, so the function body can safely rely on whatever the constraint guarantees. Constraints can reference other type parameters in the same generic, which is how you express "this key must actually belong to this object":

Typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Ada", email: "ada@example.com" };

const name = getProperty(user, "name");     // inferred as string
const id = getProperty(user, "id");         // inferred as number
// getProperty(user, "age");                // Error: "age" is not a key of the inferred user type

K extends keyof T means "K must be one of the actual property-name keys of T" — not just any string. T[K] (an indexed access type) then reads off the exact type of that specific property, so getProperty(user, "name") is known to return string, and getProperty(user, "id") is known to return number, both verified by the compiler, not just guessed at runtime.

Conditional types

A conditional type picks between two types based on a check, using syntax that mirrors JavaScript's ternary operator but operates entirely on types, evaluated at compile time:

Typescript
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;   // "yes"
type B = IsString<number>;   // "no"

This becomes genuinely useful once combined with generics, letting a type's shape depend on what it's instantiated with:

Typescript
type ApiResult<T> = T extends { error: string }
  ? { success: false; error: string }
  : { success: true; data: T };

type SuccessResult = ApiResult<{ id: number; name: string }>;
// { success: true; data: { id: number; name: string } }

type FailureResult = ApiResult<{ error: string }>;
// { success: false; error: string }

The infer keyword

infer lets a conditional type extract a piece of another type mid-check, rather than just branching on a yes/no condition — it's how TypeScript's own built-in ReturnType<T> and Parameters<T> utility types are actually implemented:

Typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;   // string
type B = UnwrapPromise<number>;             // number — not a Promise, so T itself is returned unchanged

async function fetchUser(): Promise<{ id: number; name: string }> {
  return { id: 1, name: "Ada" };
}

type FetchedUser = UnwrapPromise<ReturnType<typeof fetchUser>>;
// { id: number; name: string } — extracted through two layers: ReturnType, then unwrapping the Promise

Reading T extends Promise<infer U> ? U : T: "if T is a Promise of something, capture that 'something' as a new type variable U and return it; otherwise, just return T unchanged." infer is what makes this an extraction rather than a fixed check against one specific shape.

Mapped types

A mapped type builds a new object type by iterating over the properties of an existing one, transforming each property the same way — the mechanism underlying Partial<T>, Readonly<T>, and Record<K, V>, all of which you can now read the actual implementation of:

Typescript
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

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

type PartialUser = MyPartial<User>;
// { id?: number; name?: string; email?: string }

[K in keyof T] iterates over every key of T; ?: marks each resulting property optional; T[K] keeps that property's original type. This is (almost) the actual definition of TypeScript's built-in Partial<T>.

Mapped types can add modifiers (readonly, ?) or, with a - prefix, deliberately remove them — turning an already-optional or already-readonly type back into a fully required, mutable one:

Typescript
type MyRequired<T> = {
  [K in keyof T]-?: T[K];    // -? strips the optional modifier
};

type MyMutable<T> = {
  -readonly [K in keyof T]: T[K];   // -readonly strips the readonly modifier
};

A practical example: a typed form-validation errors map

Combining a mapped type with a conditional type solves a genuinely common real-world problem: given a data shape, produce a matching "errors" shape where every field can optionally hold an error message — and, crucially, nested object fields recurse into their own error shape instead of being treated as a single opaque value:

Typescript
type ValidationErrors<T> = {
  [K in keyof T]?: T[K] extends object ? ValidationErrors<T[K]> : string;
};

interface SignupForm {
  email: string;
  password: string;
  address: {
    street: string;
    zipCode: string;
  };
}

const errors: ValidationErrors<SignupForm> = {
  email: "Email is required",
  address: {
    zipCode: "Zip code must be 5 digits",
    // street is omitted — every field is optional, exactly where there's no error
  },
};

T[K] extends object ? ValidationErrors<T[K]> : string reads as: "for each field, if its value is itself an object (like address), recurse into a nested ValidationErrors for that shape; otherwise, the error for that field is just a string." The result is a fully typed errors object that mirrors SignupForm's own structure exactly, with every field optional and every leaf typed as string — the compiler will flag errors.address.city as invalid, because SignupForm.address never had a city field to begin with.

Comparing the tools

Tool Answers the question Built-in example using it
Generic constraint (T extends X) "What must T be allowed to do?" function logLength<T extends { length: number }>(x: T)
Conditional type (T extends X ? A : B) "Which of two shapes should this become, based on T?" Awaited<T>, Exclude<T, U>
infer "What's the piece of T I need to pull out?" ReturnType<T>, Parameters<T>
Mapped type ([K in keyof T]) "How should every property of T be transformed?" Partial<T>, Readonly<T>, Record<K, V>

Common mistakes

  • Writing a conditional type so broad it always resolves to the same branch regardless of what's passed in — a useful conditional type should meaningfully change its result across the types you actually intend to use it with.
  • Forgetting that mapped types are homomorphic by default (they preserve readonly/? modifiers from the source type unless you explicitly add -readonly/-?) — a common source of confusion when a mapped type doesn't behave the way a similarly-named built-in utility type does.
  • Reaching for a deeply recursive conditional/mapped type combination when a much simpler, explicit interface would be clearer and easier for teammates to read — these tools are powerful, but readability still matters more than cleverness in most real codebases.
  • Using infer without fully working through what happens in the "else" branch — an infer-based conditional type still needs a sensible fallback for inputs that don't match the pattern being extracted.

Interview questions

Q: What does infer do inside a conditional type, and what problem does it solve that a plain conditional type can't? A plain conditional type (T extends X ? A : B) can only check whether T matches a shape and pick between two fixed outcomes. infer lets you capture a piece of T as a new type variable while performing that check, so the resulting type can be built from something extracted out of T itself — for example, pulling the fulfilled value type out of Promise<T>, which is exactly how TypeScript's built-in Awaited<T> works.

Q: How would you write a mapped type that makes every property of an interface optional, and how does that relate to Partial<T>? type MyPartial<T> = { [K in keyof T]?: T[K] } — it iterates every key K of T via keyof T, and marks each resulting property optional with ?: while keeping its original type T[K]. This is (essentially) the actual built-in implementation of Partial<T> in TypeScript's standard library of utility types — Partial<T>, Required<T>, Readonly<T>, and Record<K, V> are all just commonly-needed mapped types the language ships for you so you don't have to write them yourself.