Expert Level · Lesson 17

Advanced
Types

Conditional types, mapped types, template literal types, and infer — the deepest corners of TypeScript's type system.

What You'll Learn OVERVIEW

These are the tools that TypeScript power users use to build utility libraries and expressive type systems.

Conditional Types
Type that changes based on a condition — like an if/else for types.
Mapped Types
Transform every property in a type — the basis of Partial, Required, Readonly.
Template Literal Types
Build string types by combining other types — powerful for event names, CSS properties, etc.
infer
Extract a type from within another type — used in utility types like ReturnType.
Intersection Types
Combine multiple types into one with & — the type must satisfy all of them.

Conditional Types T extends U ? X : Y

A conditional type selects one of two possible types based on whether a condition is true.

conditional.ts
// Basic conditional type: T extends U ? X : Y
type IsString<T> = T extends string ? "yes" : "no";

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

// Practical: unwrap a Promise type
type Awaited<T> = T extends Promise<infer R> ? R : T;

type C = Awaited<Promise<string>>; // string
type D = Awaited<number>;         // number (not a Promise, passes through)

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

type StrOrNumArr = ToArray<string | number>;
// string[] | number[]  (distributes over the union)

Mapped Types { [K in keyof T]: ... }

Mapped types iterate over all keys in a type and transform each property — this is exactly how Partial, Required, and Readonly work under the hood.

mapped.ts
interface User {
  name: string;
  age: number;
  email: string;
}

// Make every property optional (how Partial<T> works)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make every property readonly (how Readonly<T> works)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Transform every value type to a getter function
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

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

type UserStringKeys = StringKeys<User>; // "name" | "email"

Template Literal Types `${string}`

Build new string types by combining other types — like template literals at the type level.

template-literal.ts
// Combine string unions
type Color  = "red" | "green" | "blue";
type Shade  = "light" | "dark";
type Palette = `${Shade}-${Color}`;
// "light-red" | "light-green" | "light-blue"
// | "dark-red" | "dark-green" | "dark-blue"

// Typed event names
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent  = EventName<"click">;  // "onClick"
type ChangeEvent = EventName<"change">; // "onChange"

// CSS property builder
type Side = "top" | "right" | "bottom" | "left";
type MarginProp = `margin-${Side}`;
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"

// Extract route params from a URL string
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
    ? Param : never;

type Params = ExtractRouteParams<"/users/:id/posts/:postId">;
// "id" | "postId"  🤯

The infer Keyword TYPE INFERENCE

infer lets you capture a type from within a conditional type — pulling types out of other types.

infer.ts
// Get the return type of a function
type MyReturnType<T> =
  T extends (...args: any[]) => infer R ? R : never;

function getUser() { return { name: "Alice", age: 25 }; }
type UserData = MyReturnType<typeof getUser>;
// { name: string; age: number }

// Get first argument type
type FirstArg<T> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never;

type Arg = FirstArg<(name: string, age: number) => void>;
// string

// Unwrap a Promise (this is what Awaited<T> does)
type UnwrapPromise<T> =
  T extends Promise<infer V> ? V : T;

type E = UnwrapPromise<Promise<User>>; // User

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

type F = ElementType<string[]>; // string
type G = ElementType<number[]>; // number

🧠 Mental model: Think of infer R as a "capture variable". When TypeScript pattern-matches the type, it assigns the matched portion to R, which you can then use on the right side.

Intersection Types A & B

intersection.ts
interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

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

// Must implement ALL interfaces
type LoggableUser = User & Serializable & Loggable;

const user: LoggableUser = {
  name: "Alice", age: 25,
  serialize() { return JSON.stringify(this); },
  log() { console.log(this.name); }
};

// Intersection for mixins
function merge<T, U>(a: T, b: U): T & U {
  return { ...a, ...b } as T & U;
}

const combined = merge({ name: "Alice" }, { age: 25 });
// combined: { name: string } & { age: number }
// combined.name ✅   combined.age ✅

Discriminated Unions PATTERN

A powerful pattern for modeling state machines and API responses safely.

discriminated-union.ts
// Each variant has a "discriminant" field (kind/type/status)
type Shape =
  | { kind: "circle";    radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle";  base: number;  height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;   // TS knows: radius exists
    case "rectangle":
      return shape.width * shape.height;     // TS knows: width, height exist
    case "triangle":
      return 0.5 * shape.base * shape.height; // TS knows: base, height exist
  }
}

// Exhaustiveness check — TypeScript errors if a case is missing
function assertNever(x: never): never {
  throw new Error("Unexpected value: " + x);
}

Pro tip: Discriminated unions + exhaustiveness checking = compile-time guarantee that you've handled every case. Add a default: assertNever(shape) to catch missing branches.

Previous
Lesson 16 — TypeScript + Node.js
Course complete!
17 / 17 🎉
All Done!
Back to Course Home