Checkpoint: Applied TypeScript

Reviewed & published by Brayan K

This checkpoint consolidates the applied half of the course — declaration merging, namespaces, ambient types, async types, DOM types, generic defaults, function types, variadic tuples, advanced utilities, decorators, and migration — into one build challenge and a 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 You'll Consolidate

1. Recap: The Applied Toolkit

Here's the whole applied half on one page. Each idea below earned its own lesson; together they're what you reach for when typing real code.

// RECAP demo: a fetch + Awaited pipeline — async types + Promise.all + utility
// types working together (the shapes you'd extract with Awaited<ReturnType<...>>).

function fetchUser(id) {
  return Promise.resolve({ id, name: "User-" + id, roles: ["member"] });
}
function fetchPosts(id) {
  return Promise.resolve([{ title: "Hello" }, { title: "World" }]);
}

// In .ts: Awaited<ReturnType<typeof fetchUser>> = { id, name, roles } — resolved shape.
async function buildProfile(id) {
  const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);
  return user.name + " (" + posts.length + " posts, role " + user.roles[0] + ")";
}

async function main() {
  console.log(await buildProfile(7)); // User-7 (2 posts, role member)
}
main();

2. 🏗️ Build Challenge: A Typed Event Bus

Build a small event bus: createEventBus() with on(event, fn) (returning an unsubscribe function) and emit(event, payload). Get the runtime behaviour right first, then reveal the fully typed solution to see how a generic event map makes it type-safe per event.

// 🏗️ BUILD CHALLENGE — a typed EVENT BUS.
// In .ts you'd type it with a generic event map so on/emit are type-safe per event
// (see the full solution below). Here, build the RUNTIME behaviour:
//
// 1. createEventBus() returns an object with on(event, fn) and emit(event, payload).
// 2. on() registers a listener AND returns an "off" function that removes it.
// 3. emit() calls every listener registered for that event with the payload.
//
// Make these calls produce the expected output:
//   const bus = createEventBus();
//   const off = bus.on("login", (u) => console.log("Welcome " + u.name));
//   bus.on("logout", () => console.log("Goodbye"));
//   bus.emit("login", { name: "Ada" });   // Welcome Ada
//   bus.emit("logout");                    // Goodbye
//   off();                                 // unsubscribe the login handler
//   bus.emit("login", { name: "Grace" });  // (nothing — handler removed)
//   console.log("done");
//
// ✅ Expected output:
//    Welcome Ada
//    Goodbye
//    done

// your code here

First, the runnable plain-JS solution (this is exactly what the typed version compiles to):

function createEventBus() {
  const handlers = {};               // event name -> array of listeners
  return {
    on(event, fn) {
      (handlers[event] = handlers[event] || []).push(fn);
      return () => {                 // return an "off" unsubscribe function
        handlers[event] = handlers[event].filter((h) => h !== fn);
      };
    },
    emit(event, payload) {
      (handlers[event] || []).forEach((fn) => fn(payload));
    },
  };
}

const bus = createEventBus();
const off = bus.on("login", (u) => console.log("Welcome " + u.name));
bus.on("logout", () => console.log("Goodbye"));

bus.emit("login", { name: "Ada" });   // Welcome Ada
bus.emit("logout");                    // Goodbye
off();                                 // unsubscribe the login handler
bus.emit("login", { name: "Grace" });  // (nothing — handler removed)
console.log("done");

And the fully typed TypeScript, using a generic event map so each event's payload is checked:

// A fully typed event bus using a generic EVENT MAP.
type EventMap = {
  login: { name: string };
  logout: void;
};

type Handler<T> = (payload: T) => void;
type Unsubscribe = () => void;

function createEventBus<M>() {
  // Partial record: each key maps to an array of handlers for that event.
  const handlers: { [K in keyof M]?: Handler<M[K]>[] } = {};

  return {
    on<K extends keyof M>(event: K, fn: Handler<M[K]>): Unsubscribe {
      (handlers[event] ??= []).push(fn);
      return () => {
        handlers[event] = handlers[event]!.filter((h) => h !== fn);
      };
    },
    emit<K extends keyof M>(event: K, payload: M[K]): void {
      handlers[event]?.forEach((fn) => fn(payload));
    },
  };
}

const bus = createEventBus<EventMap>();
const off = bus.on("login", (u) => console.log("Welcome " + u.name)); // u: { name: string }
bus.on("logout", () => console.log("Goodbye"));
bus.emit("login", { name: "Ada" }); // ✅ payload type-checked against EventMap
// bus.emit("login", { id: 1 });    // ❌ wrong payload shape is caught
off();

3. 📝 Checkpoint Quiz

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

Interfaces are open and designed to be extended, so TypeScript merges their members via declaration merging. Type aliases are closed definitions — declaring one twice is a duplicate-identifier error. This is why library-facing, augmentable shapes are usually interfaces.

number. Awaited<T> recursively unwraps every Promise layer to the final resolved value type, exactly as await does at runtime.

Because the selector might not match anything. The | null forces you to handle the "not found" case before using the element. Use the generic (querySelector<HTMLInputElement>) to get a more specific element type.

Only when nothing else determines the type parameter — the caller didn't supply it and TypeScript couldn't infer it from an argument. If inference can find a type, the inferred type wins over the default.

It receives the original method plus a context object, and returns a replacement function (or nothing). At its core it's a higher-order function that wraps the method — for example to add logging or timing.

Add // @ts-check at the top of a .js file (or set allowJs + checkJs) and describe types with JSDoc. You get editor type-checking with zero runtime change, before committing to a .ts rename or build step.

Common Errors (and the fix)

📚 Keep Learning

🎯 Mini-Challenge: Extend the Bus With once()

Stretch goal — add a once(event, fn) that auto-unsubscribes after the first emit. Start from your build-challenge solution, add the method, and confirm the listener fires only once.

🎉 Checkpoint Complete

Practice quiz

Why do two same-name interfaces merge, but two same-name 'type' aliases error?

  • Interfaces are global and type aliases are local
  • Type aliases merge but interfaces error
  • Interfaces are open and support declaration merging; type aliases are closed, so re-declaring one is a duplicate-identifier error
  • Both merge automatically

Answer: Interfaces are open and support declaration merging; type aliases are closed, so re-declaring one is a duplicate-identifier error. Interfaces are designed to be extended, so TypeScript merges their members. A type alias is a closed definition; declaring it twice is a duplicate-identifier error.

What is 'Awaited<Promise<Promise<number>>>'?

  • number
  • Promise<number>
  • Promise<Promise<number>>
  • never

Answer: number. Awaited<T> recursively unwraps every Promise layer to the final resolved value type — here, number — exactly as 'await' does at runtime.

What is the typical return type of 'document.querySelector("#x")'?

  • Element
  • HTMLElement
  • Node[]
  • Element | null

Answer: Element | null. The selector might match nothing, so the type is 'Element | null', forcing you to handle the not-found case. Use the generic form for a more specific element type.

When does a generic's DEFAULT type parameter actually apply?

  • Always, overriding inference
  • Only when the caller didn't supply it and TypeScript couldn't infer it from an argument
  • Only in strict mode
  • Never — defaults are not allowed on generics

Answer: Only when the caller didn't supply it and TypeScript couldn't infer it from an argument. A default applies only when nothing else determines the type parameter. If inference can find a type from an argument, the inferred type wins over the default.

How does typing an event bus with '<M>' and '<K extends keyof M>' help?

  • It makes emit/on type-safe per event, so each event's payload is checked against the event map
  • It makes the bus run faster
  • It removes the need for handlers
  • It allows any payload for any event

Answer: It makes emit/on type-safe per event, so each event's payload is checked against the event map. A generic event map M plus 'K extends keyof M' ties each event key to its payload type, so emit('login', ...) demands the right shape and on() infers the handler arg.

What does 'Promise.all' return when given a tuple of differently-typed promises?

  • An array of 'any'
  • A single merged object
  • A promise of a tuple preserving each element's resolved type and position
  • void

Answer: A promise of a tuple preserving each element's resolved type and position. Promise.all over a tuple resolves to a tuple of the awaited types in the same positions, so destructuring keeps each value precisely typed.

What does a method decorator receive and return in TS 5 (stage-3 decorators)?

  • The class instance and returns void only
  • The original method plus a context object, and returns a replacement function (or nothing)
  • Only the property name
  • The prototype and a property descriptor to mutate

Answer: The original method plus a context object, and returns a replacement function (or nothing). A stage-3 method decorator receives the original method and a context object and may return a replacement function — at its core a higher-order function wrapping the method.

What does the 'declare' keyword do in an ambient declaration?

  • It generates runtime JavaScript
  • It imports a module
  • It freezes an object
  • It describes the types of existing runtime code without emitting any output

Answer: It describes the types of existing runtime code without emitting any output. 'declare' provides type information for code that already exists at runtime (globals, libraries) and produces no emitted JavaScript. DefinitelyTyped (@types) ships many such declarations.

What do 'Parameters<F>' and 'ReturnType<F>' extract from a function type F?

  • Its name and length
  • A tuple of its parameter types, and its return type, respectively
  • Its 'this' type and prototype
  • Its decorators

Answer: A tuple of its parameter types, and its return type, respectively. Parameters<F> yields a tuple of F's argument types; ReturnType<F> yields F's return type. Both are built with conditional types and 'infer'.

What is a safe first step to adopt TypeScript without renaming files?

  • Rewrite everything as '.ts' at once
  • Type everything as 'any'
  • Add '// @ts-check' to a '.js' file (or set allowJs + checkJs) and describe types with JSDoc
  • Disable strict mode permanently

Answer: Add '// @ts-check' to a '.js' file (or set allowJs + checkJs) and describe types with JSDoc. '// @ts-check' plus JSDoc (or allowJs + checkJs) gives editor type-checking with zero runtime change, before committing to a '.ts' rename or build step.

Continue this course

Frequently asked questions

What does this checkpoint cover?

It pulls together the applied half of the course: declaration merging, namespaces, ambient declarations and @types, typing Promises and async/await, DOM and event types, generic defaults and inference, function types with this, variadic tuples, advanced utility types, decorators, and migrating JavaScript to TypeScript. The build challenge and quiz exercise these together rather than one at a time.

How should I approach the build challenge?

Start from the runtime behaviour in the starter: a createEventBus that supports on, emit, and an unsubscribe function. Get the plain-JS version producing the expected output first, then study the fully typed solution to see how a generic event map makes on/emit type-safe per event. Building the runtime, then layering types on, mirrors a real migration.

Why is the typed solution shown separately from the runnable code?

Because the runner executes plain JavaScript — type annotations are erased at runtime. The runnable box proves the behaviour works; the typed solution in the 'Show solution' block shows the real TypeScript, including the generic event map and the mapped-type handler record that the editor would check for you.

What should I do after this checkpoint?

Move on to TypeScript with React, where these applied skills come together in a component framework: typing props and state, events in JSX, hooks with generics, and async data loading. Everything you practised here — utility types, function types, async types, and migration habits — carries directly into typing a real React app.

Related lessons