Without notes, state yesterday’s main idea and one unresolved question.
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'sLoadStateand today'sResultare aliases. - Interfaces merge; aliases do not. Declare
interface Componenttwice in the same scope and TypeScript combines the two into one. Declaretype Componenttwice and you geterror 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
- Convert
Componentfrom atypealias to aninterface. Confirmnpx tsc --noEmitstays silent — nothing else should need to change. - Add an optional
datasheetUrl?: stringand areadonly id. WritedatasheetLabel(item: Component): stringthat handles the missing case. - Break both deliberately and record the exact errors in
notes/day-33.md: usedatasheetUrlwithout checking (expect TS18048), and reassignid(expect TS2540). Fix both. - Create
src/types.tswithResult<T>andPaginated<T>plusokandfail. - Write
parseStock(input: string): Result<number>and use it on your form input, renderingr.errorwhen the parse fails. - Write
pageOf<T>(...)and call it twice — once withstring[], once withComponent[]. Note in your file, as a comment, the two concrete typesTtook. - Deliberately access
r.valuewithout checkingr.ok. Record the TS2339 error, then fix it with anif. - 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 buildand 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
Tis alwaysComponent, writeComponent. - Optional versus nullable.
stock?: numbermeans the key may be missing;stock: number | nullmeans it is present and may be blank. Day 15's distinction, now in the type. - Expecting
readonlyto protect at runtime. It is erased at build. It documents and enforces intent while you write, and nothing more. - Mixing
interfaceandtypeat 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.
- 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.
- Generics describes a way to restrict what
Tmay be, using a keyword. Name it and write one sentence on when it would be useful forpageOf.
Add both answers to notes/day-33.md before committing.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Create Result
and Paginated types, then use them in small examples. - 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
Typed result and pagination examples without needless abstraction.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.