Lesson 08 / 23
Type Aliases
The differences between a type declaration and an interface: naming any type, the absence of declaration merging, circular-reference rules, a class contract written with a type alias, and a selection criterion.
Contents
The same measurement model was written with type in the previous topic and with
interface in this topic’s first lesson. The two declaration forms are interchangeable
in most cases; they are not equivalent.
A type alias is a declaration that gives a name to an existing type. It does not
create a new type — after type Id = string, Id and string are the same type and
interchangeable. An interface, on the other hand, defines a named object type. This
lesson establishes where the difference shows up and when to choose which.
Naming Any Type
An alias can name any type. An interface can only define object and callable shapes:
type Unit = "C" | "Pa" | "%"; type Id = string; type Interval = [low: number, high: number]; type Converter = (value: number) => number; const unit: Unit = "C"; const interval: Interval = [-40, 85]; const toKelvin: Converter = (v) => v + 273.15; console.log(unit, interval, toKelvin(21.4));
Output:
C [ -40, 85 ] 294.54999999999995
None of these four can be written with an interface. A union, a primitive type, and a tuple are not object types; a function type can be written with an interface, but it needs much longer syntax. The mapped, conditional, and template literal types from the next topic can likewise only be named with an alias.
Notice the output’s last value: 21.4 + 273.15 gives 294.54999999999995, not
294.55. The type system correctly counted the number type; floating-point
representation’s precision limit is not its concern. This is a concrete example of the
“what it does not prove” list from the first lesson.
No Declaration Merging
Interfaces could be redeclared under the same name and merged. Type aliases have no such thing:
type Measurement = { id: string }; type Measurement = { value: number };
h2.ts(1,6): error TS2300: Duplicate identifier 'Measurement'. h2.ts(2,6): error TS2300: Duplicate identifier 'Measurement'.
The diagnostic appears twice, because both declarations are party to the conflict.
This is not a limitation of the alias but a guarantee: a type alias’s definition is complete where it is written. It is impossible for another file to add a field to that type. In your own codebase’s data models, this predictability is usually what you want.
In the other direction, an interface can be derived from a type alias through extension:
type NumericMeasurement = { id: string; value: number }; interface TimedMeasurement extends NumericMeasurement { time: number; } const m: TimedMeasurement = { id: "s-01", value: 21.4, time: 1 }; console.log(m);
This file compiles without error. But if the alias does not name an object type, extension fails:
type NumericMeasurement = { id: string; value: number }; type TextMeasurement = { id: string; text: string }; type Entry = NumericMeasurement | TextMeasurement; interface Extended extends Entry { time: number; }
h7.ts(5,28): error TS2312: An interface can only extend an object type or intersection of object types with statically known members.
Extending a union has no meaning: which member Extended inherits from is ambiguous.
Writing the same operation with an intersection, though, is valid — type Extended = Entry & { time: number } produces a union that adds a time field to both members.
Circular Reference
When measurement records turn into a nested structure, the type has to refer to itself. A reference inside an object field is valid:
type MeasurementNode = { id: string; value: number; children: MeasurementNode[]; }; const root: MeasurementNode = { id: "boiler-2", value: 21.4, children: [{ id: "s-01", value: 21.4, children: [] }], }; function sumNodes(node: MeasurementNode): number { return 1 + node.children.reduce((t, c) => t + sumNodes(c), 0); } console.log(sumNodes(root));
The file compiles without error; its output is 2.
A direct circular reference, by contrast, is rejected:
type Chain = Chain | null; declare const z: Chain; console.log(z);
h3.ts(1,6): error TS2456: Type alias 'Chain' circularly references itself.
The distinction: the MeasurementNode reference sits inside an object field, and the
compiler resolves that field’s type only when needed. The Chain declaration, though,
needs itself again to resolve its own definition — an expansion that never terminates.
The rules for recursive type definitions will be covered in detail in the Advanced
Types topic.
A Class Contract Written with a Type Alias
The contract a class satisfies can also be written with a type alias. As long as the
alias names an object type, the implements declaration works exactly as it does with
an interface:
type Measurement = { id: string; value: number }; class FileRecord implements Measurement { constructor( readonly id: string, readonly value: number, ) {} } const record: Measurement = new FileRecord("s-01", 21.4); console.log(record.id, record.value);
Output is s-01 21.4.
The limit is the same as with extension: if the alias names a union, it cannot be implemented.
type NumericMeasurement = { id: string; value: number }; type TextMeasurement = { id: string; text: string }; type Entry = NumericMeasurement | TextMeasurement; class FileRecord implements Entry { constructor(readonly id: string) {} }
u2.ts(5,29): error TS2422: A class can only implement an object type or intersection of object types with statically known members.
The reasoning is the same: which member the class satisfies is ambiguous. A class being “either this shape or that one” cannot be expressed with a single declaration. In such a model, the contract is satisfied by separate classes for each union member, and the union is only used at the value level.
Selection Criterion
The differences between the two declaration forms can be gathered into a table:
| Criterion | interface |
type |
|---|---|---|
| Defines an object type | Yes | Yes |
| Names a union, tuple, primitive | No | Yes |
| Redeclaration under the same name | Merges | TS2300 |
| Extension | extends (checked at declaration) |
& (silently collapses) |
| Implemented by a class | implements |
implements (if object type) |
| Mapped, conditional type | No | Yes |
An actionable criterion follows from the table:
An interface for contracts shaped like an object. A data record’s fields, the methods a service offers, the contract a class satisfies. Because extension is checked, errors are caught at declaration time; diagnostic messages carry the name.
A type alias for everything else. Unions, discriminated unions, tuples, function types, types computed at the type level. These either have no interface equivalent or forcing one is awkward.
In this course, the measurement record itself is written with an interface, and its statuses and derived types are written with type aliases:
interface Measurement { readonly id: string; sensor: string; value: number; unit: "C" | "Pa" | "%"; time: number; } type MeasurementStatus = | { status: "valid"; measurement: Measurement } | { status: "failed"; id: string; code: string }; function summarize(record: MeasurementStatus): string { return record.status === "valid" ? `${record.measurement.id}=${record.measurement.value}${record.measurement.unit}` : `${record.id}: ${record.code}`; } console.log( summarize({ status: "valid", measurement: { id: "s-01", sensor: "temperature", value: 21.4, unit: "C", time: 1706000000000, }, }), ); console.log(summarize({ status: "failed", id: "s-02", code: "E17" }));
Output:
s-01=21.4C s-02: E17
The division of labor between the two declaration forms shows here: Measurement is a
record’s shape and can grow; MeasurementStatus is a closed set of options, and its
growth should be a deliberate decision.
Summary
- A type alias names an existing type; it can name any type, including unions, tuples, primitives, and function types.
- Interfaces get redeclared under the same name and merge; a type alias’s second
declaration produces a
TS2300diagnostic, which guarantees the definition is complete in one place. - An interface can extend a type alias that names an object type; it cannot extend a union type.
- A circular reference inside an object field is valid; a directly circular type
alias produces a
TS2456diagnostic. - A type alias naming an object type can be implemented with
implements; one naming a union produces aTS2422diagnostic. - Criterion: object-shaped contracts are written with an interface; unions and types computed at the type level are written with a type alias.
Next Step
The contracts so far have defined only data and operation signatures; the implementation came from a separate function each time. Classes hold the two together, and TypeScript adds visibility control to classes. The next lesson takes up access modifiers, abstract classes, and what they leave behind in the compiled output.
To keep your progress and take notes, Log in
My notes
Log in to take notes.