Checkpoint: The Type System

Reviewed & published by Brayan K

This checkpoint consolidates everything from the type-system unit — unions and intersections, literals, enums, narrowing, generics, object types, overloads, and tuples — into one recap, a hands-on build challenge, 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 You'll Consolidate

📚 Unit Recap

Here's the whole type-system unit on one page. Each row links back to its full lesson if you want a refresher.

ConceptIn one lineExample
UnionEITHER one type OR anotherstring | number
IntersectionALL types combined into oneA & B
LiteralOne exact value as a type"on" | "off"
EnumNamed set of constantsenum Dir { N, S }
NarrowingProve a type before usetypeof x === "string"
Type guardFunction returning x is FooisCat(a): a is Cat
GenericType variable chosen by calleridentity<T>(x: T): T
Optional / readonlyMay be absent / can't changenickname?: string
Index signatureOpen keys, one value type[k: string]: number
OverloadSeveral call shapes, one bodylen(s: string): number
Array / tupleList / fixed-shape positionsnumber[] · [string, number]

💡 The Big Picture

Everything in this unit answers one question: "what shape is this value, and what can I safely do with it?" Unions and literals describe the possibilities, narrowing and guards confirm which one you have, generics keep your helpers reusable without losing that knowledge, and object/array/tuple types pin down structure. The build challenge below uses several of these together — exactly how real code does.

🎯 Build Challenge: A Shape Domain

Model a small shape domain as a discriminated union, write an area function that narrows on the kind field, and add a generic sumBy helper to total the areas. Fill in the two blanks, run it, and match the expected output — then compare against the full TypeScript solution.

// 🎯 BUILD CHALLENGE: a tiny "shape" domain with a discriminated union
//                     and a GENERIC helper. Model it, then use it at runtime.
//
// The TypeScript shape (for reference — the runnable code is plain JS below):
//   type Shape =
//     | { kind: "circle"; radius: number }
//     | { kind: "square"; side: number }
//     | { kind: "rect"; width: number; height: number };
//
// STEP 1 — area(shape): switch on shape.kind and return the area:
//   circle -> Math.PI * radius^2
//   square -> side * side
//   rect   -> width * height
//
// STEP 2 — a GENERIC helper  sumBy(items, fn)  that maps each item through fn
//   and sums the numbers it returns. (In TS: sumBy<T>(items: T[], fn: (x: T) => number): number)
//
// STEP 3 — build an array of shapes and log:
//   - the area of a single circle of radius 2  (expect 12.57 to 2 d.p.)
//   - the TOTAL area of [circle r=1, square side=2, rect 3x4] via sumBy
//     (expect 19.14 to 2 d.p.)
//
// Fill in the blanks marked ___ :

function area(shape) {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius * shape.radius;
    case "square": return shape.side * shape.side;
    case "rect":   return ___;                 // 👉 shape.width * shape.height
    default:       return 0;
  }
}

function sumBy(items, fn) {
  let total = 0;
  for (const item of items) total += ___;     // 👉 fn(item)
  return total;
}

const shapes = [
  { kind: "circle", radius: 1 },
  { kind: "square", side: 2 },
  { kind: "rect", width: 3, height: 4 },
];

console.log(area({ kind: "circle", radius: 2 }).toFixed(2)); // 12.57
console.log(sumBy(shapes, area).toFixed(2));                 // 19.14

// ✅ Expected output:
//    12.57
//    19.14
// A discriminated union — each member tagged by a literal 'kind':
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rect"; width: number; height: number };

// Narrowing on 'kind' lets each branch use that member's own fields:
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2;
    case "square": return shape.side * shape.side;
    case "rect":   return shape.width * shape.height;
    default: {
      // Exhaustiveness check: if a new Shape member is added and not handled,
      // this line becomes a compile error.
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

// A GENERIC helper — works for ANY element type T:
function sumBy<T>(items: T[], fn: (x: T) => number): number {
  return items.reduce((total, item) => total + fn(item), 0);
}

const shapes: Shape[] = [
  { kind: "circle", radius: 1 },
  { kind: "square", side: 2 },
  { kind: "rect", width: 3, height: 4 },
];

area({ kind: "circle", radius: 2 }); // 12.566...  -> "12.57"
sumBy(shapes, area);                 // 19.141...  -> "19.14"

🔁 Rapid Self-Check

Predict every line of output before you run it — it touches narrowing, discriminated unions, generics, and tuple destructuring in one go.

// A quick self-check you can RUN. Predict each line before you press play.

// 1) typeof narrowing
function kindOf(x) {
  if (typeof x === "string") return "text";
  if (typeof x === "number") return "num";
  return "other";
}
console.log(kindOf("hi"), kindOf(7), kindOf(true)); // text num other

// 2) discriminated union
function label(r) {
  return r.ok ? "OK:" + r.data : "ERR:" + r.error;
}
console.log(label({ ok: true, data: "x" }));   // OK:x
console.log(label({ ok: false, error: "no" })); // ERR:no

// 3) generic identity keeps the value
function id(v) { return v; }
console.log(id([1, 2, 3])); // [ 1, 2, 3 ]

// 4) tuple destructuring
const [a, b] = ["Ada", 36];
console.log(a, b); // Ada 36

📝 Checkpoint Quiz

Answer each in your head, then expand to check. Six questions covering the whole unit.

A | B (union) means the value is either an A or a B — you must narrow before using members unique to one. A & B (intersection) means the value is both at once — it has every member of A and B, so all are safe to use immediately.

Because let is reassignable, TypeScript widens the inferred type to string. Use const, an explicit literal annotation, or as const to keep the narrow literal type "dark".

It's a type predicate. At runtime the function returns a boolean, but the predicate tells the compiler: "if this returned true, treat the argument as Fish from here on." Without it, TypeScript sees a plain boolean and won't narrow.

The generic preserves the input type in the output, so callers keep full type safety downstream. any throws that information away, silencing the very checks TypeScript exists to provide. Generics give reuse and safety.

When the return type depends on which argument type you passed — e.g. make("point") returns an object but make("name") returns a string. A single union signature can't express that input-to-output link; overloads can.

[string, number] is a tuple: exactly two elements, a string then a number, order fixed. string[] is an array: any number of elements, all strings. Tuples pin down shape; arrays describe "a list of one type".

🎉 Checkpoint Complete

Practice quiz

What does the union type 'string | number' allow?

  • A value that is both a string and a number at once
  • Only strings, with numbers coerced automatically
  • A value that is either a string or a number
  • An array containing strings and numbers

Answer: A value that is either a string or a number. A union 'A | B' means the value is EITHER an A OR a B. You must narrow before using members unique to one side.

What does the intersection type 'A & B' describe?

  • A value that is both A and B at once, having every member of each
  • A value that is only A or only B
  • The properties A and B have in common
  • A tuple of an A followed by a B

Answer: A value that is both A and B at once, having every member of each. An intersection 'A & B' combines all types into one — the value has every member of A AND B, so all are safe to use immediately.

Which is a literal type?

  • string
  • number[]
  • keyof T
  • 'dark' | 'light'

Answer: 'dark' | 'light'. A literal type is one exact value used as a type. "'dark' | 'light'" is a union of two string literal types.

Why might 'let mode = "dark"' be inferred as 'string' rather than '"dark"'?

  • TypeScript never infers literal types
  • Because 'let' is reassignable, so TypeScript widens the type to 'string'
  • Because string literals are not valid types
  • Because 'dark' is a reserved word

Answer: Because 'let' is reassignable, so TypeScript widens the type to 'string'. Since 'let' allows reassignment, TS widens to 'string'. Use 'const', an explicit literal annotation, or 'as const' to keep the literal type.

In a type guard, what does the return type 'pet is Fish' do?

  • Tells the compiler to narrow pet to Fish when the function returns true
  • Converts pet into a Fish at runtime
  • Throws an error if pet is not a Fish
  • Declares pet as a global Fish variable

Answer: Tells the compiler to narrow pet to Fish when the function returns true. 'pet is Fish' is a type predicate. At runtime it returns a boolean, but it tells the compiler to treat the argument as Fish when true.

Why is a generic 'identity<T>(x: T): T' better than 'identity(x: any): any'?

  • It runs faster at runtime
  • It allows passing more arguments
  • It preserves the input type in the output, keeping type safety
  • There is no real difference

Answer: It preserves the input type in the output, keeping type safety. The generic preserves the relationship between input and output types. 'any' throws that information away, silencing TypeScript's checks.

How do you mark an object property as optional?

  • nickname!: string
  • nickname?: string
  • readonly nickname: string
  • optional nickname: string

Answer: nickname?: string. The '?' after the property name makes it optional, so the property may be absent on values of that type.

What does the index signature '[k: string]: number' mean?

  • The object has exactly one key named k
  • The object is read-only
  • Keys must be numbers
  • Any string key maps to a number value

Answer: Any string key maps to a number value. An index signature describes open-ended keys: any string key is allowed, and every value must be a number.

When do function overloads beat a single union signature?

  • When you want fewer lines of code
  • When the return type depends on which argument type was passed
  • When the function takes no arguments
  • Overloads are never useful in TypeScript

Answer: When the return type depends on which argument type was passed. Overloads express that the return type depends on the input type (e.g. make('point') vs make('name')) — something a single union signature cannot capture.

What is the difference between '[string, number]' and 'string[]'?

  • They are identical
  • 'string[]' has exactly two elements; the tuple has any number
  • '[string, number]' is a fixed two-element tuple; 'string[]' is any-length array of strings
  • Tuples cannot hold different types

Answer: '[string, number]' is a fixed two-element tuple; 'string[]' is any-length array of strings. A tuple '[string, number]' pins down length and per-position types; 'string[]' is a list of any number of strings.

Continue this course

Frequently asked questions

What should I be able to do after this checkpoint?

You should be able to model a small domain using a discriminated union, narrow values safely (typeof, instanceof, in, custom guards), write and use generic functions and types, describe object shapes with optional/readonly/index signatures, write function overloads, and type arrays and tuples — including readonly variants. The build challenge exercises several of these at once.

I'm shaky on one topic — what should I revisit?

Jump back to the specific lesson: Union & Intersection, Literal Types, Enums, Type Narrowing, Generics, Object Types, Function Overloads, or Arrays & Tuples. Each has runnable examples and a mini-challenge. Re-doing a lesson's 'Your Turn' and Mini-Challenge is the fastest way to firm up a weak spot.

Why do these type features disappear when the code runs?

TypeScript is a compile-time layer over JavaScript. Types, interfaces, generics, overloads, and 'this' parameters are erased during compilation, leaving plain JavaScript. They exist to catch mistakes and document intent while you write code — which is exactly why the interactive editor here runs the type-stripped JavaScript.

What comes after the type system?

Next you'll learn Utility Types — built-in generic helpers like Partial, Pick, Omit, Record, and ReturnType that transform existing types into new ones. They build directly on the generics, object types, and unions you consolidated here, so this checkpoint is the perfect launchpad.

Related lessons