0 / 91
Week 5 · Day 33 of 91

Interfaces, type aliases, and generics

TypeScript, Packages, and Tooling

Objective

Choose simple type abstractions that improve readability.

TypeScript is similar to design-rule checking: it catches invalid connections before runtime but cannot prove system behavior.

  • interfaces versus aliases
  • optional and readonly properties
  • basic generic containers

Why this matters

You now have types for one thing: a component. Today you learn to describe patterns that repeat across different things — "an operation that either succeeded with a value or failed with a message", "one page out of a longer list" — without writing the same shape five times. That is what generics are for. The equally important half of today is knowing when not to reach for them: an abstraction that does not remove real repetition makes code harder to read while feeling clever.

Interfaces versus type aliases

You have been using type aliases (type Component = { ... }). TypeScript has a second way to name an object shape, the interface:

export interface Component {
  id: string;
  name: string;
  category: "passive" | "active" | "connector";
  stock: number;
}

For describing an object, these two are interchangeable. Everything you did on Day 31 works with either word. The real differences are narrow:

  • Only a type alias can name something that is not an object. Unions, primitives, and tuples all need type. type Category = "passive" | "active" cannot be written as an interface — which is why yesterday's LoadState and today's Result are aliases.
  • Interfaces merge; aliases do not. Declare interface Component twice in the same scope and TypeScript combines the two into one. Declare type Component twice and you get error TS2300: Duplicate identifier 'Component'. Merging is occasionally useful for extending types from a library, and is a footgun in your own code, because a second declaration you did not expect changes the first silently.
  • Interfaces extend with extends; aliases combine with &. Both work.

The honest guidance: pick one and be consistent within a project. Interfaces for object shapes and type aliases for everything else is a common, defensible split, and the one this lesson uses. What matters far more than the choice is that a reader never has to wonder why file A uses one and file B the other.

Optional and readonly properties

Two modifiers change what a property means, and both remove a category of bug.

A ? after the name makes the property optional — it may be absent.

export interface Component {
  readonly id: string;
  name: string;
  stock: number;
  datasheetUrl?: string;
}

The type of datasheetUrl is now string | undefined, so you cannot use it without checking:

console.log(item.datasheetUrl.length);
error TS18048: 'item.datasheetUrl' is possibly 'undefined'.

That is the same narrowing rule as yesterday, arriving through a different door. Check first:

export function datasheetLabel(item: Component): string {
  if (item.datasheetUrl === undefined) return "No datasheet";
  return item.datasheetUrl.toUpperCase();
}

readonly means the property may be set when the object is created and never reassigned afterwards.

const c: Component = { id: "r1", name: "Resistor 10k", stock: 1 };
c.id = "r2";
error TS2540: Cannot assign to 'id' because it is a read-only property.

Use it on identity fields — an id, a created-at date — anything that being able to change is itself the bug. Note the limit: readonly is a compile-time rule only. It is erased at build time, so nothing stops JavaScript from changing that property at runtime.

Optional is DNP; readonly is the trimmed-and-sealed pot

A BOM line marked DNP — do not populate — is a footprint that exists on the board and may hold no part. You must check before assuming a component is there; that is ?. A trim pot set at test and then locked with sealant is readonly: adjusted once during assembly, never afterwards. And the sealant is a design intent, not a law of physics — someone with a screwdriver can still break it, exactly as readonly disappears at runtime.

Basic generic containers

A generic is a type with a blank in it. You fill the blank in at each use, and the rest of the shape stays the same. The blank is written in angle brackets and conventionally called T.

export type Paginated<T> = {
  items: T[];
  page: number;
  pageSize: number;
  totalCount: number;
};

Paginated<Component> has items: Component[]. Paginated<string> has items: string[]. One definition, any contained type — that is the whole idea: a reusable type shape for different contained types. Without generics you would write PaginatedComponents, PaginatedUsers, PaginatedRecords, identical except for one line each.

A generic is a package, not a part

"SO-8" describes a body outline, a pin count, and a pad pitch. It says nothing about what is inside — op-amp, memory, regulator. The package is reusable precisely because it is silent about the part. Paginated<T> is the package; T is the part. Paginated<Component> is one populated footprint.

The labelled bin

A parts bin has a fixed shape, a lid, and a slot for a label. "Bin of resistors" and "bin of screws" are the same bin — only the label and the contents differ. You do not design a new bin for every kind of part, and you do not lose track of which is which, because the label says. T is the label.

The second pattern worth having is Result<T> — an operation that either produced a value or failed with a reason, modelled as a discriminated union:

export type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

true and false are literal types here, so ok is the discriminant and narrowing works exactly as it did yesterday. Functions can be generic too — the <T> goes before the parameter list:

export function ok<T>(value: T): Result<T> {
  return { ok: true, value };
}

export function fail<T>(error: string): Result<T> {
  return { ok: false, error };
}

Reaching for the wrong branch is caught:

const r = parseStock("12");
console.log(r.value);
error TS2339: Property 'value' does not exist on type 'Result<number>'.
  Property 'value' does not exist on type '{ ok: false; error: string; }'.

Which forces the correct shape at every call site:

const r = parseStock("12");
if (r.ok) {
  console.log(r.value + 1);
} else {
  console.log(r.error);
}

Do not generify by reflex

A generic earns its place when the same shape is genuinely used with different contained types — at least twice, in code you have already written. A Result<T> used once, with T always number, is just { ok: true; value: number } | { ok: false; error: string } written in a harder-to-read way. Repetition you have actually met justifies an abstraction; repetition you imagine does not.

Walkthrough

In src/types.ts, define Result<T> and Paginated<T> plus the ok and fail helpers above, then write two small users of them.

export function parseStock(input: string): Result<number> {
  const n = Number(input);
  if (!Number.isFinite(n)) return fail("Not a number");
  if (n < 0) return fail("Stock cannot be negative");
  return ok(n);
}

export function pageOf<T>(all: T[], page: number, pageSize: number): Paginated<T> {
  const start = (page - 1) * pageSize;
  return {
    items: all.slice(start, start + pageSize),
    page,
    pageSize,
    totalCount: all.length,
  };
}

pageOf never looks inside an element, which is exactly why it can be generic. Call it two ways and confirm the compiler tracks the element type:

const page = pageOf(["a", "b", "c"], 1, 2);
console.log(page.items.length, page.totalCount);
2 3

You did not write pageOf<string> — TypeScript inferred T from the argument. Now hover over page.items in VS Code: it reads string[]. Call pageOf(myComponents, 1, 20) and the same hover reads Component[]. One function, two element types, no any.

npx tsc --noEmit

Silence means every one of those shapes agrees.

Checkpoint

Explain why Result and Paginated must be type aliases and cannot be interfaces. (Result is a union; Paginated could be an interface, and saying so out loud is the point.)

How to use AI today

Today's mode is reviewer. A review should produce specific, actionable findings with evidence — a named type, a reason it does not pay for itself, and the simpler version. Today, point it at your own cleverness.

Reviewer mode, once your types compile

"Review these types for overengineering. Remove abstractions that do not clarify repeated structure." Judge each finding by one test: does this abstraction remove repetition that exists in my code right now? Delete any that fail, including ones the review defended.

Your turn

  1. Convert Component from a type alias to an interface. Confirm npx tsc --noEmit stays silent — nothing else should need to change.
  2. Add an optional datasheetUrl?: string and a readonly id. Write datasheetLabel(item: Component): string that handles the missing case.
  3. Break both deliberately and record the exact errors in notes/day-33.md: use datasheetUrl without checking (expect TS18048), and reassign id (expect TS2540). Fix both.
  4. Create src/types.ts with Result<T> and Paginated<T> plus ok and fail.
  5. Write parseStock(input: string): Result<number> and use it on your form input, rendering r.error when the parse fails.
  6. Write pageOf<T>(...) and call it twice — once with string[], once with Component[]. Note in your file, as a comment, the two concrete types T took.
  7. Deliberately access r.value without checking r.ok. Record the TS2339 error, then fix it with an if.
  8. Last step, and the real deliverable: reread your own types and delete any generic used with only one concrete type. Write one line in your notes for each one you kept, naming the two or more places it is used. Run npm run build and commit.

You are done when

You have typed result and pagination examples without needless abstraction: every generic in the project is used with at least two different contained types, and you can name them.

Common pitfalls

  • Generic-for-one-type. The most common overengineering in a beginner codebase. If T is always Component, write Component.
  • Optional versus nullable. stock?: number means the key may be missing; stock: number | null means it is present and may be blank. Day 15's distinction, now in the type.
  • Expecting readonly to protect at runtime. It is erased at build. It documents and enforces intent while you write, and nothing more.
  • Mixing interface and type at random. Both work; inconsistency costs the next reader time looking for a difference that is not there.

Verify it yourself

Open today's reference, the TypeScript Handbook, and read Everyday Types (the interfaces section) and Generics.

  1. Find the Handbook's own comparison of interfaces and type aliases. Does it agree that declaration merging is the main behavioural difference? Note anything else it lists.
  2. Generics describes a way to restrict what T may be, using a keyword. Name it and write one sentence on when it would be useful for pageOf.

Add both answers to notes/day-33.md before committing.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Create Result and Paginated types, then use them in small examples.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

Typed result and pagination examples without needless abstraction.

Working with AI today

AI as skeptical reviewer

Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.

Review these types for overengineering. Remove abstractions that do not clarify repeated structure.

References

End-of-day quiz

Q1 What does a generic type parameter provide?
Q2 Which result best proves today’s work is complete?
Q3 What should an AI code review primarily produce?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.