Checkpoint: Advanced Types

Reviewed & published by Brayan K

This checkpoint consolidates the advanced half of the course — utility types, mapped types, conditional types and infer, generic constraints, keyof/typeof, discriminated unions, modules, and tsconfig — into one recap, one build challenge that combines several features, and a short quiz.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What This Checkpoint Covers

1. Quick Recap (run it)

Each line below exercises one advanced feature at runtime: a utility-type-style pick, a mapped-type-style key walk, a keyof getter, and a discriminated-union switch. Run it to refresh all of them at once.

// === Recap: the advanced features you've learned, exercised at runtime ===
const user = { id: 1, name: "Alice", email: "a@x.com", admin: false };

// Utility-type idea — Pick a subset (runtime: build the smaller object)
const summary = { id: user.id, name: user.name };
console.log("Pick:", summary);                  // Pick: { id: 1, name: 'Alice' }

// Mapped-type idea — walk every key and transform the value
const stringified = {};
for (const k of Object.keys(user)) stringified[k] = String(user[k]);
console.log("Mapped:", stringified);
// Mapped: { id: '1', name: 'Alice', email: 'a@x.com', admin: 'false' }

// keyof + generic getter — read a property by a real key
function get(obj, key) { return obj[key]; }
console.log("Getter:", get(user, "email"));     // Getter: a@x.com

// Discriminated union — narrow by a tag
function area(shape) {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.r ** 2;
    case "square": return shape.s ** 2;
  }
}
console.log("Union:", area({ kind: "square", s: 3 })); // Union: 9

📚 Cheat-Sheet: When to Reach for What

You want to…Reach for
Make all fields optional / drop a key / select keysPartial / Omit / Pick
Type a dictionary or lookup tableRecord<K, V>
Transform every property of a type{ [K in keyof T]: ... }
Choose a type based on a conditionT extends U ? X : Y
Extract a type from inside anotherinfer
Restrict a generic / give it a fallback<T extends C = D>
Read a property by a safe keykeyof T + T[K]
Model mutually-exclusive statesdiscriminated union

2. Build Challenge: A Typed Event Bus

Time to combine three features in one program: a discriminated union of events, a mapped-type handler registry (one handler per event type), and a generic constraint so you can only register a handler for a known event type. Finish the starter so the test prints the expected lines, then reveal the full TypeScript solution to compare.

// 🎯 BUILD CHALLENGE: a typed event bus
// Combine THREE advanced features at runtime:
//   1. a DISCRIMINATED UNION of events (tagged by "type")
//   2. a MAPPED-TYPE handler registry (one handler per event type)
//   3. a GENERIC CONSTRAINT so on(type, handler) only accepts a known type
//
// In TypeScript the types would be:
//   type AppEvent =
//     | { type: "login";   userId: number }
//     | { type: "logout";  userId: number }
//     | { type: "message"; from: number; text: string };
//   type Handlers = { [K in AppEvent["type"]]: (e: Extract<AppEvent, { type: K }>) => void };
//
// Finish createBus() below so the test at the bottom prints the expected lines.

function createBus() {
  const handlers = {};                 // the mapped-type registry (type -> handler)

  function on(type, handler) {
    // 1) Register the handler under its event type, then return api for chaining
    handlers[___] = handler;           // 👉 type
    return api;
  }

  function emit(event) {
    // 2) Look up the handler by the discriminant field, then call it
    const handler = handlers[event.___]; // 👉 type  (the discriminant)
    if (!handler) throw new Error("no handler for " + event.type);
    return handler(event);
  }

  const api = { on, emit };
  return api;
}

const log = [];
const bus = createBus()
  .on("login",   (e) => log.push("user " + e.userId + " logged in"))
  .on("message", (e) => log.push(e.from + " says: " + e.text));

bus.emit({ type: "login", userId: 7 });
bus.emit({ type: "message", from: 7, text: "hello" });
console.log(log.join("\n"));

// ✅ Expected output:
//    user 7 logged in
//    7 says: hello
// 1) Discriminated union of events — tagged by "type"
type AppEvent =
  | { type: "login";   userId: number }
  | { type: "logout";  userId: number }
  | { type: "message"; from: number; text: string };

// Helper: pull the one event whose "type" is K (a conditional + infer pattern,
// here via the built-in Extract utility, which is itself a conditional type)
type EventOf<K extends AppEvent["type"]> = Extract<AppEvent, { type: K }>;

// 2) Mapped type — one handler per event type. Key remapping not needed;
//    we map over the union of discriminant literals AppEvent["type"].
type Handlers = {
  [K in AppEvent["type"]]?: (event: EventOf<K>) => void;
};

interface Bus {
  // 3) Generic constraint — K must be a known event type
  on<K extends AppEvent["type"]>(type: K, handler: (e: EventOf<K>) => void): Bus;
  emit(event: AppEvent): void;
}

function createBus(): Bus {
  const handlers: Handlers = {};

  const api: Bus = {
    on(type, handler) {
      // store keyed by the literal type; cast bridges the per-K handler types
      (handlers as Record<string, (e: any) => void>)[type] = handler as any;
      return api;
    },
    emit(event) {
      const handler = (handlers as Record<string, (e: any) => void>)[event.type];
      if (!handler) throw new Error("no handler for " + event.type);
      handler(event);   // 'event' is narrowed by its discriminant
    },
  };
  return api;
}

const bus = createBus()
  .on("login",   (e) => console.log("user " + e.userId + " logged in"))
  .on("message", (e) => console.log(e.from + " says: " + e.text));

bus.emit({ type: "login", userId: 7 });
bus.emit({ type: "message", from: 7, text: "hello" });
// user 7 logged in
// 7 says: hello

Note: the small as casts bridge the gap between the precise per-event handler types and a single runtime map — a common, contained use of a type assertion inside an otherwise fully-typed API.

Common Errors (and the fix)

📝 Checkpoint Quiz

Answer each in your head, then expand to check. No peeking first!

Omit<User, "id">. Omit keeps every property except the ones you name, so it's the idiomatic way to strip server-managed fields from an input type.

{ readonly [K in keyof T]: T[K] } — this is exactly the built-in Readonly<T>. Adding -readonly instead would strip read-only from every property.

X captures the function's return type; this is the built-in ReturnType<F>. infer introduces X inside the conditional's extends pattern.

Constraining K to keyof T rejects keys that don't exist (a typo becomes a compile error), and the lookup type T[K] returns the exact property type instead of a vague any.

Exhaustiveness. If every case is handled, x narrows to never and compiles; if someone adds a new variant and forgets a case, x isn't never and the build fails — turning a missed case into a compile error.

strictNullChecks (enabled by "strict": true). It makes null and undefined distinct types you must handle — via a guard, optional chaining ?., or ?? — before using a value.

🎉 Checkpoint Complete

Practice quiz

Which utility type builds a 'create' payload that is a User without its server-generated 'id'?

  • Pick<User, 'id'>
  • Partial<User>
  • Omit<User, 'id'>
  • Record<'id', User>

Answer: Omit<User, 'id'>. Omit<User, 'id'> keeps every property except the named ones, so it strips the server-managed 'id' from the input type.

Which mapped type makes every property of T read-only, and what built-in is it?

  • { readonly [K in keyof T]: T[K] } — Readonly<T>
  • { [K in keyof T]?: T[K] } — Partial<T>
  • { -readonly [K in keyof T]: T[K] } — Mutable<T>
  • { [K in keyof T]: T[K] | null } — Nullable<T>

Answer: { readonly [K in keyof T]: T[K] } — Readonly<T>. Adding the 'readonly' modifier in the mapped type produces the built-in Readonly<T>. Using '-readonly' would instead strip read-only.

In 'type R<F> = F extends (...a: any[]) => infer X ? X : never', what does X capture?

  • The function's parameter types
  • The function name
  • The 'this' type of the function
  • The function's return type — this is ReturnType<F>

Answer: The function's return type — this is ReturnType<F>. 'infer X' captures the function's return type inside the conditional's extends pattern. This is exactly the built-in ReturnType<F>.

What does 'infer' do inside a conditional type?

  • Runs type inference at runtime
  • Introduces a new type variable to capture part of a matched type
  • Forces a type assertion
  • Loops over the keys of a type

Answer: Introduces a new type variable to capture part of a matched type. 'infer' declares a fresh type variable inside the 'extends' clause of a conditional type, capturing a sub-type from the matched shape.

Why does a getter use '<T, K extends keyof T>(obj: T, key: K): T[K]' instead of typing key as string?

  • Constraining K to keyof T rejects invalid keys and returns the exact property type T[K]
  • It is shorter to write
  • It allows any key including ones that don't exist
  • string keys are not allowed in TypeScript

Answer: Constraining K to keyof T rejects invalid keys and returns the exact property type T[K]. K extends keyof T rejects typo'd keys at compile time, and the lookup type T[K] yields the precise property type instead of 'any'.

What does the type operator 'keyof T' produce?

  • The values of T
  • A new object type
  • A union of T's property names (keys)
  • The number of keys in T

Answer: A union of T's property names (keys). 'keyof T' yields a union of the literal key names of T, which you can then constrain a generic parameter to.

In a switch over a discriminated union, what does an 'assertNever(x)' default branch buy you?

  • Faster runtime dispatch
  • Exhaustiveness — a forgotten case fails to compile because x is no longer 'never'
  • Automatic handling of new cases
  • Nothing; it is decorative

Answer: Exhaustiveness — a forgotten case fails to compile because x is no longer 'never'. If every case is handled, x narrows to 'never' and compiles. Add a variant and forget a case, and x is not 'never', so the build fails.

How does 'Record<K, V>' help when typing a dictionary or lookup table?

  • It makes all properties optional
  • It removes keys from an existing type
  • It extracts the return type of a function
  • It builds an object type whose keys are K and whose values are all V

Answer: It builds an object type whose keys are K and whose values are all V. Record<K, V> constructs an object type with the given key set K mapped to value type V — ideal for dictionaries and lookup tables.

What does the lookup type 'T[K]' give you?

  • The key K itself
  • The type of the property at key K within T
  • A boolean for whether K exists
  • All keys of T

Answer: The type of the property at key K within T. T[K] is an indexed-access (lookup) type: it resolves to the type of the property at key K in T.

Which tsconfig setting most reduces 'Cannot read property of undefined' crashes?

  • noImplicitAny
  • allowJs
  • strictNullChecks (enabled by 'strict': true)
  • esModuleInterop

Answer: strictNullChecks (enabled by 'strict': true). strictNullChecks makes null and undefined distinct types you must handle (via guards, ?., or ??) before using a value.

Continue this course

Frequently asked questions

How do I know I'm ready to move past the advanced-types section?

You're ready if you can do these without looking them up: derive a new type from an existing one with Partial/Pick/Omit/Record; write a mapped type with { [K in keyof T]: ... } and a modifier; read a conditional type T extends U ? X : Y and use infer to extract a sub-type; constrain a generic with <T extends ...> and a default; explain keyof, the typeof type operator, and a lookup type T[K]; and model mutually-exclusive states with a discriminated union plus an exhaustiveness check. The build challenge on this page exercises most of them at once.

These features feel academic — when do I actually use them?

Constantly, often without noticing. Every React component props type that reuses part of another type uses Pick/Omit. Form libraries, ORMs, and routers use keyof + generics for type-safe field/column/param access. State management (useReducer, Redux) is discriminated unions end to end. Library type definitions lean on mapped and conditional types so that your call sites stay precisely typed. You don't have to author exotic types daily, but reading them — and reaching for the right one when a type starts to duplicate — is everyday senior TypeScript.

What's the single most valuable habit from this section?

Derive types instead of duplicating them. The moment you copy an interface and tweak a field, ask whether a utility (Partial, Omit, Pick, Record), a mapped type, or a conditional type could compute it from the source instead. Derived types can't drift out of sync, document intent, and shrink the surface area for bugs. Pair that with strict mode on and discriminated unions for state, and most whole categories of runtime errors simply stop happening.

Related lessons