AdvancedTypeScript · Lesson 1 of 10

Advanced Types: Mapped, Conditional & Template Literal

Compute new types from existing ones with keyof, mapped and conditional types.

keyof T produces a union of an object type's keys; T[K] looks up the type of a property. typeof value lifts a runtime value's type into the type world, and as const keeps literal values instead of widening them.

Mapped types loop over keys to build a new type — for example, turning every property into a getter or making them all optional. Conditional types (T extends U ? X : Y) choose a type based on another, and infer captures part of a type, such as a Promise's resolved value.

Template literal types build string types from other types, which is great for event names, routes and CSS-like tokens.

advanced-types.tsTypeScript
const ROLES = ["admin", "teacher", "student"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "teacher" | "student"

interface User {
  id: number;
  name: string;
  role: Role;
}

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// { getId: () => number; getName: () => string; getRole: () => Role }

type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string[]>>; // string[]

type NonFunctionKeys<T> = {
  [K in keyof T]: T[K] extends (...args: never[]) => unknown ? never : K;
}[keyof T];

type EventName = `${"user" | "order"}:${"created" | "deleted"}`;
// "user:created" | "user:deleted" | "order:created" | "order:deleted"

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

function makeGetters<T extends object>(obj: T): Getters<T> {
  const out: Record<string, () => unknown> = {};
  for (const key of Object.keys(obj) as (keyof T & string)[]) {
    out["get" + key[0].toUpperCase() + key.slice(1)] = () => obj[key];
  }
  return out as Getters<T>;
}

const users: User[] = [{ id: 1, name: "Amina", role: "teacher" }];
const names: string[] = pluck(users, "name");
const g = makeGetters(users[0]);
const ev: EventName = "order:created";
const sample: A = ["typed"];
type DataKeys = NonFunctionKeys<User & { save(): void }>; // "id" | "name" | "role"
const key: DataKeys = "role";

console.log(names, g.getRole(), ev, sample, key, ROLES);

Key points

  • as const + typeof X[number] derives a union from a runtime array — one source of truth.
  • Mapped types transform every key; as in a mapped type renames keys.
  • Use conditional and infer types sparingly — readability still matters.

Exercise

Write a type DeepReadonly<T> that makes nested objects and arrays readonly. Then write type Routes = "/users" | "/users/:id" and a type ParamsOf<R> that extracts { id: string } from "/users/:id".

Show solution

Try the exercise yourself first — then compare your approach with this one.

DeepReadonly is a recursive mapped type: primitives and functions stay as they are, arrays become ReadonlyArrays of deep-readonly items, and objects get readonly keys whose values are made deep-readonly too.

ParamsOf uses template literal types with infer to peel one :param segment at a time and collects each name as a string property.

deep-types.tsTypeScript
type DeepReadonly<T> = T extends (...args: never[]) => unknown
  ? T
  : T extends readonly (infer U)[]
    ? ReadonlyArray<DeepReadonly<U>>
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

type ParamsOf<R extends string> = R extends `${string}:${infer Param}/${infer Rest}`
  ? { [K in Param]: string } & ParamsOf<`/${Rest}`>
  : R extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

interface School {
  name: string;
  forms: { level: number; streams: string[] }[];
}

const school: DeepReadonly<School> = {
  name: "Azania",
  forms: [{ level: 4, streams: ["A", "B"] }],
};
// school.forms[0].streams.push("C");   // error: push does not exist on ReadonlyArray
// school.forms[0].level = 5;            // error: level is read-only

type Routes = "/users" | "/users/:id";
type UserParams = ParamsOf<"/users/:id">;                          // { id: string }
type ResultParams = ParamsOf<"/schools/:schoolId/results/:term">;  // { schoolId: string } & { term: string }

const p1: UserParams = { id: "42" };
const p2: ResultParams = { schoolId: "S1", term: "2" };
const route: Routes = "/users/:id";
console.log(school.forms[0].streams, p1, p2, route);

Check your understanding

  1. What does keyof { id: number; name: string } produce?

  2. Given const ROLES = ["admin", "teacher"] as const, what is (typeof ROLES)[number]?

  3. In T extends Promise<infer U> ? U : T, what does infer U do?

  4. What does a template literal type like ` ${"user" | "order"}:created ` produce?

Ask AI