Skip to content

Volume ITechnology

3.7.2 — The Type Vocabulary ​

3.7.1 set up the machine; this page teaches it to speak. Every TypeScript feature downstream — narrowing, generics, type-level programming — composes the vocabulary built here, so we take each construct properly: syntax, what the checker does with it, where it's used in real code, and the corners that bite. By the end, string | { kind: "err"; code: number }[] reads like a sentence.

1. Primitives, inference, and literal types ​

The primitives mirror JavaScript's (3.6.7): string, number, boolean, bigint, symbol, null, undefined. But you'll write them less than you expect, because of type inference:

typescript
let count = 42;            // inferred: number       — annotation would be noise
const name = "Ada";        // inferred: "Ada"        — note: the LITERAL type!
let title = "Dr";          // inferred: string       — let may be reassigned, so widened

Two lessons hide in those three lines. First, the working style: annotate function signatures and public boundaries; let inference handle locals — inference is exact, tireless, and never drifts from the code. Second, meet the literal type: "Ada" is a type with exactly one value. const infers the literal; let widens to string (it could be reassigned). Literal types look trivial until you union them:

typescript
type Status = "pending" | "active" | "closed";   // a checked, zero-cost enumeration

function advance(s: Status) { /* … */ }
advance("actve");        // ❌ Argument of type '"actve"' is not assignable

A union of literals gives you exhaustive, typo-proof enumerations with no runtime construct at all — one of TypeScript's best features, and (as section 5 argues) usually a better answer than enum. Autocomplete reads the union; 3.7.3's exhaustiveness checking polices every switch over it.

2. Arrays and tuples ​

typescript
const xs: number[] = [1, 2, 3];              // array: any length, one element type
const pair: [string, number] = ["Ada", 36];  // tuple: FIXED length, per-slot types

An array is homogeneous and unbounded; a tuple assigns a type to each position — the shape of "several values traveling together": React's useState returns [T, setter], Object.entries yields [string, V] pairs. The full tuple toolkit:

typescript
type Point3 = [x: number, y: number, z: number];   // named slots — pure documentation
type Range  = [start: number, end?: number];        // optional slot
type Row    = readonly [id: number, ...cells: string[]];  // readonly + rest ("variadic")

Named elements label positions in tooltips (nothing at runtime); rest elements type "a fixed head then more" — how 3.7.4 types ...args. And readonly matters doubly here: readonly T[] / ReadonlyArray<T> forbids push/splice/index-assignment — the honest type for function parameters you promise not to mutate. Mutable arrays are assignable to readonly, never the reverse.

Indexing honesty deserves its warning: xs[10] types as number even though it's undefined at runtime — unless 3.7.1's noUncheckedIndexedAccess is on, which makes it number | undefined and forces the check. Turn it on.

3. Object types, and the modifiers that shape them ​

typescript
type User = {
  id: string;                    // required
  name: string;
  nickname?: string;             // (1) optional — may be absent
  readonly createdAt: Date;      // (2) readonly — assignment is a compile error
};

(1) ? means the property may be missing, and its type becomes string | undefined on read — under strictNullChecks you must handle that before use. (2) readonly forbids reassignment at compile time — shallow, erased, purely intent-enforcement (the runtime counterpart is Object.freeze, 3.6.4).

When keys aren't known in advance, an index signature types the pattern instead:

typescript
type Scores = { [player: string]: number };       // any string key → number
type Flags  = Record<string, boolean>;            // same idea, utility-type spelling

One structural rule explains a whole family of "why won't it compile": excess property checking. Object literals assigned directly to a typed slot are checked for unknown keys ({ name: "Ada", nmae: "oops" } errors — it's a typo detector); the same object passed via a variable is allowed (structural typing's normal "extra properties are fine" — 3.7.7). Literal = strict, indirect = structural.

4. interface vs type — the honest answer ​

Both declare object shapes, and nine times out of ten they're interchangeable:

typescript
interface Point { x: number; y: number }
type     Point2 = { x: number; y: number };   // same checking, same erasure

The real differences: type can name anything — unions, tuples, primitives, conditional types (type ID = string | number has no interface equivalent) — while interface is objects/callables only. interface supports declaration merging — two declarations of the same name merge their members, which is how library consumers augment Express.Request or Window (3.7.6) — type forbids redeclaration. Interfaces extends (and classes implements) with slightly better error messages and (historically) friendlier caching; types compose with &. A sane team rule: interface for public object contracts (extendable, mergeable), type for everything else — unions, function types, compositions. Arguing beyond that is bikeshedding.

Intersections compose shapes: type Employee = Person & { salary: number } requires all members of both. Caveat: intersecting incompatible members quietly produces never fields — intersections are set-intersection on values, not a merge operation.

5. Enums — honestly assessed ​

enum predates literal unions and is TypeScript's most debated feature — notably, one of the few that emits runtime code (3.7.1):

typescript
enum Direction { Up, Down }          // numeric: Up = 0, Down = 1
enum Level { Info = "INFO", Warn = "WARN" }   // string enum — safer

Direction[0];        // → "Up" — numeric enums emit a REVERSE-mapped object

Numeric enums have real flaws: any number was historically assignable to them, the reverse mapping bloats output, and values are opaque in logs. String enums fix opacity but remain nominal-ish islands (you must import the enum to use it) and still emit objects. const enum inlines values and emits nothing — but breaks under isolated-module transpilers (esbuild/swc, 3.7.1), so bundler-based repos ban it. Modern default: a union of string literals (type Level = "info" | "warn") — zero runtime, structural, log-readable, exhaustiveness-checkable; add an as const object (3.7.3) when you also need runtime iteration over the values. Reach for enum only when a team convention or API surface genuinely wants the namespaced object.

6. The special types: any, unknown, never, void ​

Four special types answer every question about which value can go where. Picture them as a ladder. unknown is at the top: every type can be assigned to it, because it promises nothing. never is at the bottom: it can be assigned to anything, but nothing can be assigned to it, because no value of that type exists. And any is the escape hatch that pretends to be wherever it is needed.

unknown — the honest topanything fits in; nothing usable until narrowedstring · numberobjects · arraysunions · literalsnever — the empty bottomno value exists; assignable to allanyopts OUT entirely
Figure 1 — The assignability lattice. Arrows read "is assignable to." unknown accepts everything and permits nothing; never is the empty type at the bottom; any (amber) sits outside the lattice — it satisfies every check by disabling checking.

any switches type checking off for that value — every operation allowed, errors silent — and it's contagious: values computed from any are any. It exists for migration and genuine escape hatches; casual use silently deletes the safety you adopted TypeScript for. unknown is the type-safe counterpart: you may hold one, but may not use it until you've proven what it is (3.7.3):

typescript
function handle(a: any, u: unknown) {
  a.toUpperCase();          // compiles — and may explode at runtime
  u.toUpperCase();          // ❌ 'u' is of type 'unknown'
  if (typeof u === "string") u.toUpperCase();   // ✅ narrowed, safe
}

Rule: unknown at every boundary where data arrives from outside (API responses, JSON.parse, catch variables); any only as tracked debt. ⚑Difference between never and unknown in TypeScript. [EQ-55]

never is the type with no values — where unknown is "could be anything," never is "nothing can be here." You meet it in three places:

  • As the return type of functions that never return normally, because they always throw or loop forever.
  • As the type of a variable after every member of a union has been ruled out. That is what powers exhaustiveness checking, built in 3.7.3.
  • In type-level programming, where it means "drop this" (3.7.5).

void is milder than never. It means "returns normally, but with nothing worth using". One quirk worth knowing: a callback typed () => void does accept a function that returns a value. That is on purpose, so arr.forEach(cb) can take any function.

7. The expert lens ​

Model data as unions from day one. The vocabulary's center of gravity is not interface — it's the union. Real domain states are alternatives: loading or loaded or failed; guest or member or admin. Teams that model these as one fat interface of optionals ({ data?: T; error?: E; loading?: boolean }) create illegal states that type-check (loading && error); teams that model them as a union of precise variants make illegal states unrepresentable — the discriminated-union pattern 3.7.3 completes. When a shape sprouts its third optional property, ask whether it's secretly a union.

Inference is the pleasant to use contract. TypeScript's bet — infer locals, annotate boundaries — is what keeps well-typed code looking like JavaScript. Over-annotation is real debt: redundant local annotations go stale and hide inference regressions (an annotation silently coerces where inference would have flagged drift). The reflex: write the implementation, hover to check what inference concluded, annotate only where the conclusion should be narrower or is a public promise.

Runtime-emitting features are the odd ones out. enum (and namespaces, and parameter properties) date from before the erasable-layer philosophy fully won. The ecosystem's direction — literal unions, as const, erasableSyntaxOnly, Node's type stripping — is convergence on "types are only types." When choosing between two designs, prefer the one that vanishes.

Next: the checker's dynamic half — 3.7.3: how control flow refines these types, and the guard/discriminant/assertion toolkit that makes unions practical.

Recall

  • Inference types most code (const infers literal types; let widens). Style: annotate signatures/boundaries, infer locals. Unions of literals = zero-cost checked enumerations — usually better than enum (which emits runtime objects; const enum breaks strippers).
  • Tuples type positions ([string, number], named/optional/rest slots, readonly); arrays are unbounded+homogeneous; noUncheckedIndexedAccess makes indexing honest. ? = may be missing (handle undefined); readonly = compile-time no-reassign; index signatures/Record for open key sets; object literals get excess property checking.
  • interface = objects only, extends, declaration merging (augmentable — the library-contract choice); type = names anything (unions!, tuples, functions), composes with &. Pick a convention, stop arguing.
  • The ladder:
    • unknown is the top. It can hold anything, but you must prove what it is before using it. Use it wherever data comes in.
    • never is the bottom. It has no values. It is used for functions that never return, for exhaustiveness checks, and for filtering at the type level.
    • void means "returns nothing worth using".
    • any switches checking OFF, and it spreads. Use it only as tracked debt.

Self-test: Why does const s = "hi" infer differently from let s = "hi", and why does it matter for unions? When is a tuple the right type — name two real APIs shaped like one? Give the two genuine interface/type differences. Place any, unknown, never in the lattice and give each one's legitimate use.

Quiz Bank ​

FoundationalWhat is the difference between any and unknown?

Both mean "type not known", but they behave in opposite ways.

any switches checking off. Every operation compiles, and errors slip through silently. It also spreads: anything computed from an any is also any, so one leak removes the types from a whole chain of calls.

unknown is the safe version. Anything can be assigned to it. But you cannot call it, index it or do anything with it until you narrow it, with typeof, instanceof or a schema parse (3.7.3).

The practical rule: use unknown wherever data comes in, such as API responses, JSON.parse and catch variables. The setting useUnknownInCatchVariables does the last one for you. Use any only as a deliberate escape hatch that someone is tracking. In terms of the ladder: unknown is the honest top. any is not on the ladder at all. It passes every check by switching the check off.

FoundationalWhat is never, and how does it differ from void?

never is the type with no values — the empty set at the bottom of the lattice, assignable to everything (vacuously) with nothing assignable to it. It arises as: the return type of functions that never return normally (always throw or loop forever); the type remaining after control flow eliminates every member of a union — the engine of exhaustiveness checking; and the absorbing "filter it out" element in conditional types (3.7.5). void is much milder: the function returns normally but its value is meaningless (undefined) — "nothing useful," not "nothing possible." Quirk: () => void callback positions accept value-returning functions by design (so forEach can take any function); never positions accept nothing.

Appliedinterface vs type — what are the real differences, and what convention follows?

Interchangeable for plain object shapes (same checking, both erased). Real differences: (1) type can alias anything — unions (string | number), tuples, primitives, function types, conditional/mapped types — while interface only describes object/callable shapes; consequently union-centric domain modeling requires type. (2) interface supports declaration merging — multiple same-name declarations merge, which is the mechanism for augmenting library types (Express.Request, Window — 3.7.6); redeclaring a type is an error, making it tamper-proof. (3) Minor: extends gives marginally clearer errors than &; intersections silently produce never members when they conflict. Sane convention: interface for public, potentially-extendable object contracts; type for everything else — and don't let the debate consume review time.

InterviewWhy do modern TypeScript teams prefer literal unions over enums?

enum is one of the few TypeScript features that produces runtime code (3.7.1). Each kind has a problem:

  • Numeric enums produce an object that maps both ways. That adds code, puts meaningless numbers in your logs, and for a long time accepted any number at all.
  • String enums are readable, but every user has to import the enum object.
  • const enum disappears from the output, but it breaks tools that compile one file at a time, such as esbuild and swc. So most modern projects ban it.

A union of string literals, such as type Level = "info" | "warn", gives you the same checked list with no runtime code at all. Any "info" fits, with no import needed. The values are readable in logs and easy to search for. You get autocomplete, and switch statements can be checked for missing cases with never. When runtime iteration over the values is also needed, pair the union with a single as const array (const LEVELS = ["info", "warn"] as const; type Level = typeof LEVELS[number]) — one source of truth, still erasable. Enum survives where a namespaced value-object is genuinely wanted as API surface.

StaffA code review shows a state interface { data?: Result; error?: ApiError; isLoading: boolean } and downstream code full of non-null assertions. Critique the modeling and refactor it.

The interface makes illegal states representable: { isLoading: true, data, error } type-checks, as does "neither loading nor data nor error" — so downstream code can't prove which combination holds and resorts to data! non-null assertions, which are unchecked claims that rot into runtime crashes exactly when the impossible state ships. Refactor to a discriminated union, one variant per real state: type State = { status: "idle" } | { status: "loading" } | { status: "success"; data: Result } | { status: "error"; error: ApiError }. Now data exists only when status === "success" — the checker enforces access order, every ! disappears, switch (state.status) narrows each branch (3.7.3), and a never default makes adding a variant break every unhandled site at compile time. Transitions become total functions State → State, testable and typo-proof. The team principle:

model states as alternatives (unions), not as bags of optionals — every optional flag pair is 2^n phantom states, and non-null assertions downstream are the smell announcing it. This is the single highest-leverage TypeScript modeling habit; it costs nothing at runtime (erasure) and removes a class of production bugs.

Flashcards ​

Flashconst vs let inference

const infers the literal type ("Ada"); let widens (string). Literal unions = zero-cost checked enums.

FlashTuple toolkit

[string, number] per-slot; named slots for docs; optional ? slots; rest ...T[]; readonly for no-mutation. useState/Object.entries shapes.

Flashinterface vs type in one line

interface: objects, extends, declaration merging (augmentable). type: anything (unions/tuples/functions), no redeclare. Convention over debate.

FlashEnum verdict

Emits runtime objects; const enum breaks strippers. Default to literal unions (+ as const array if values needed at runtime).

FlashThe lattice

unknown = top (hold all, prove before use). never = bottom (no values; exhaustiveness). void = returns nothing useful. any = outside, checking off, contagious.

FlashExcess property checking

Object literals assigned directly are checked for unknown keys (typo detector); the same object via a variable passes (structural).

Scenario Drill ​

DrillYou're designing types for a config-driven feature-flag client: flags have a name, a kind (boolean, percentage, variant), kind-specific payloads, and consumers must get compile errors if they read the wrong payload or forget a kind. Runtime code must also iterate all kinds. Design the types and justify each choice.

The requirements name their own tools. Kinds with kind-specific payloads + wrong-read must not compile = a discriminated union keyed on kind: type Flag = { name: string; kind: "boolean"; enabled: boolean } | { name: string; kind: "percentage"; rollout: number } | { name: string; kind: "variant"; variants: readonly string[]; assignment: Record<string, string> }. Reading flag.rollout without first checking kind === "percentage" is now a compile error, and switch (flag.kind) narrows each branch to its exact payload (3.7.3).

Forgetting a kind must not compile = exhaustiveness: a default branch assigning to never breaks every consumer switch when a fourth kind is added — the change-management guarantee. Runtime iteration of kinds = the as const single-source pattern, since a pure type-level union is erased: const KINDS = ["boolean", "percentage", "variant"] as const; type Kind = typeof KINDS[number] — the array exists at runtime for iteration/validation, the union is derived from it, and they can never drift.

The other choices:

  • readonly string[] for variants, because client code must not change the config.
  • A Record<string, string> for assigning users to variants, because the set of users is open.
  • No enum. It adds runtime code and causes trouble with type-stripping tools, and the as const array already gives the list at runtime.
  • type everywhere, and no interface, because the union's members are a fixed set.

Add the ingress honesty: config arrives as JSON, so it lands as unknown and passes a schema parse before ever becoming Flag (3.7.7) — erasure means the beautiful union checks nothing about the wire. The drill's principle: discriminated union for closed alternatives, as const bridge where runtime needs the list, unknown+validation at the door.