Before you begin
Why this matters
Code that should work for several kinds of value, such as every vehicle or every payment, says what it needs from them. In TypeScript the compiler checks only the shape of a value. So it accepts values you did not plan for, and it stays silent about a member that may be missing. Knowing the rule lets you predict when the compiler will complain and when it will not.
What this helps you reason about
- Write a function that works for every value with the members it needs.
- Predict when TypeScript accepts an object with extra or missing properties.
- Keep a value’s own type when a function returns it.
A bike-share dock rents e-bikes and scooters. Its screen shows how far each one can go on its battery. The screen’s code should work for every kind of vehicle the dock rents, also kinds added next year. This lesson follows how TypeScript decides that a value is a vehicle, and which method runs when the screen asks one for its range.
Already know this? The short version
- An interface lists the members a value must have; a function that takes a Vehicle may use only those members, and v.rangeKm() runs the method of the object v refers to.
- TypeScript is structural: any value with the required members fits, with or without implements; extra properties are allowed, and an optional member may be missing.
- An object literal written directly in a call gets the excess property check; the same object from a variable does not.
- Interfaces are erased: they have no method bodies, and v instanceof Vehicle does not compile.
- function farthest<T extends Vehicle>(a: T, b: T): T keeps the caller’s own type; a function that returns Vehicle forgets it.
Words in this lesson (5)
- interface
- A declaration that names the properties and methods a value must have, such as interface Vehicle { rangeKm(): number }. Code that takes a Vehicle can use exactly those members.
- structural typing
- Checking a value by its shape: a value fits a type when it has the members the type lists, with matching types. The value’s class, or whether it has a class at all, does not matter.
- optional property
- A property marked with ? in an object type, such as note?: string. An object may leave it out, and reading it then gives undefined.
- excess property check
- An extra check on an object literal written directly where a type is expected: a property that the type does not list is a compile error. The same object stored in a variable first is accepted.
- type parameter
- A name for a type chosen by each call, written in angle brackets, such as T in function farthest<T extends Vehicle>(a: T, b: T): T. extends Vehicle is its constraint: T must be a type that fits Vehicle.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Contract
What does an interface promise?
An interface names the operations a value must support. interface Vehicle { rangeKm(): number } says: a Vehicle has a method rangeKm that returns a number. It says nothing about how the range is worked out.
A function written against the interface, dockLine(v: Vehicle), may use only what Vehicle lists. In return, it works for every value that has those members: an EBike, a Scooter, and kinds of vehicle written later. When dockLine calls v.rangeKm(), the method that runs is the one of the object v refers to.
interface Vehicle {
rangeKm(): number;
}
class EBike {
constructor(public battery: number) {}
rangeKm(): number {
return this.battery / 2;
}
}
function dockLine(v: Vehicle): string {
return v.rangeKm() + " km";
} An interface names operations; code written against it runs the operation of the value it is given.
Coming from Java: A TypeScript interface is a check made by the compiler; nothing of it exists while the program runs.
Precisely: dynamic dispatch
Choosing the method from the value while the program runs is called dynamic dispatch. In JavaScript it is a property lookup: v.rangeKm finds the function on the object, or on the prototype of its class.
Experiment
Follow a call through the interface
dockLine knows only the interface. The call v.rangeKm() runs the method of the value v holds at that moment, so one line of dockLine runs two different methods.
Why it matters Code written against an interface works for every type that satisfies it, and each call runs the method of the value it was given.
Predict both lines. Then rename rangeKm to rangekm in class Scooter only, and predict again.
interface Vehicle {
rangeKm(): number;
}
class EBike {
constructor(public battery: number) {}
rangeKm(): number {
return this.battery / 2;
}
}
class Scooter {
constructor(public battery: number) {}
rangeKm(): number {
return this.battery / 4;
}
}
function dockLine(v: Vehicle): string {
return v.rangeKm() + " km";
}
console.log(dockLine(new EBike(80)));
console.log(dockLine(new Scooter(80)));
What to look for
Each new EBike(80) and new Scooter(80) first stops in the constructor. The stepper names an extra frame <instance_members_initializer> there; it belongs to creating the object and can be ignored here. Step into dockLine, then into v.rangeKm(). The frame is named rangeKm in both calls, and this, like v, shows {battery: 80} in both: the panel does not show the class. The evidence is the highlighted line: line 8, inside class EBike, in the first call, and line 15, inside class Scooter, in the second. The run prints 40 km, then 20 km. After the edit, nothing runs: Line 24: Argument of type 'Scooter' is not assignable to parameter of type 'Vehicle'. Property 'rangeKm' is missing in type 'Scooter' but required in type 'Vehicle'.
Expected output
40 km
20 km02 / Satisfying
How does a type satisfy an interface?
EBike above never mentions Vehicle, yet dockLine(new EBike(80)) compiles. TypeScript checks a value by its shape: a value fits a type when it has every member the type requires, with matching types. This is structural typing.
Two results follow. An object with extra properties still fits: { name: "cargo bike", seats: 2, rangeKm: () => 30 } is a Vehicle below, with a seats property that dockLine cannot see. And a member marked with ?, an optional property, may be missing.
One more check applies to an object literal written directly where a Vehicle is expected. There an unknown property is probably a typing mistake, so TypeScript reports Object literal may only specify known properties, and 'seats' does not exist in type 'Vehicle'. This is the excess property check. The same object stored in a variable first is accepted.
A class can still name the interface: class EBike implements Vehicle. That adds a check at the class, so a missing method is reported where the class is written. It changes nothing while the program runs.
interface Vehicle {
name: string;
rangeKm(): number;
note?: string; // optional: may be missing
}
const cargo = { name: "cargo bike", seats: 2, rangeKm: () => 30 };
dockLine(cargo); // fits: extra seats, no note
dockLine({ name: "cargo bike", seats: 2, rangeKm: () => 30 });
// error: Object literal may only specify known properties Any value with the required members fits an interface; extra members are allowed, except in an object literal written in place.
Coming from Java: TypeScript needs no implements: a class with the right members already fits, and so does a plain object.
Coming from Go: This is like Go’s implicit interfaces, but it covers properties as well as methods.
Precisely: implements and optional members
implements Vehicle checks only the members Vehicle requires. A class whose optional method is misspelled still passes, because an optional member may be missing; code that tests for the method then finds nothing.
Experiment
Pass an object with an extra property
cargo has every member Vehicle requires, so it fits; its extra seats and its missing optional note do not matter. An object literal written directly where a Vehicle is expected gets one more check, for properties the type does not list.
Why it matters TypeScript accepts any value of the right shape. The excess property check applies only to an object literal written in place.
Predict whether the program compiles and what it prints. Then replace the two lines that use cargo with console.log(dockLine({ name: "cargo bike", seats: 2, rangeKm: () => 30 })); and predict again.
interface Vehicle {
name: string;
rangeKm(): number;
note?: string;
}
function dockLine(v: Vehicle): string {
return v.name + ": " + v.rangeKm() + " km";
}
const cargo = { name: "cargo bike", seats: 2, rangeKm: () => 30 };
console.log(dockLine(cargo));
console.log(cargo.seats);
What to look for
Before line 11 runs, cargo shows not initialized yet. After it, cargo shows {name: "cargo bike", seats: 2, rangeKm: arrow function ()}. Step into dockLine: v shows the same object, seats included, although Vehicle does not list seats. The call v.rangeKm() enters a frame named rangeKm, the arrow function. The run prints cargo bike: 30 km, then 2. After the edit, nothing runs: Line 11: Object literal may only specify known properties, and 'seats' does not exist in type 'Vehicle'.
Expected output
cargo bike: 30 km
203 / Defaults
Can the interface provide a method itself?
A TypeScript interface cannot contain code. It lists members; it cannot give a member a default body for types to keep or replace.
Shared behavior goes in a function that takes the interface. canReach below works for every Vehicle and calls each value’s own rangeKm.
// Shared code for every Vehicle: a function.
function canReach(v: Vehicle, km: number): boolean {
return v.rangeKm() >= km;
} An interface has no method bodies; write shared behavior as a function that takes the interface.
Precisely: abstract classes
An abstract class can hold both: methods that subclasses must write, and methods with bodies that they inherit. Only classes that extend it get those bodies; a plain object of the same shape does not.
04 / Interface values
What does a variable of an interface type hold?
A variable of type Vehicle holds a reference to an ordinary object, like any variable that holds an object (see Arguments, values, and references). Nothing marks the object as a Vehicle: interfaces are removed when TypeScript is compiled to JavaScript.
So a running program cannot ask whether a value is a Vehicle. v instanceof EBike is true when v was made by new EBike(…); that asks about a class, not the interface. A plain object such as cargo fits Vehicle but was made by no class of yours. v instanceof Vehicle does not compile: 'Vehicle' only refers to a type, but is being used as a value here.
const cargo: Vehicle = { rangeKm: () => 30 };
const fleet: Vehicle[] = [new EBike(80), cargo];
console.log(fleet[0] instanceof EBike); // true
console.log(fleet[1] instanceof EBike); // false An interface exists only for the compiler; at run time a Vehicle is an ordinary object.
Coming from Java: Java can test v instanceof Vehicle while the program runs; TypeScript cannot, because the interface no longer exists then.
05 / Constraints
How does generic code ask for an interface?
A function that takes Vehicles and returns one loses the caller’s type. farthestAny(a: Vehicle, b: Vehicle): Vehicle returns a Vehicle, so reading .battery on its result is an error: Property 'battery' does not exist on type 'Vehicle', even when both arguments were EBikes.
A type parameter keeps the type. In farthest<T extends Vehicle>(a: T, b: T): T, T is a type chosen by each call, and extends Vehicle is its constraint: T must fit Vehicle. Inside, the code may use what Vehicle lists. The caller gets back its own type: farthest(new EBike(80), new EBike(60)) is an EBike.
Take a Vehicle parameter when the function only uses the value. Use a type parameter when the caller needs its own type back. A later lesson on generics goes further.
function farthest<T extends Vehicle>(a: T, b: T): T {
return a.rangeKm() >= b.rangeKm() ? a : b;
}
const best = farthest(new EBike(80), new EBike(60));
console.log(best.battery); // 80: best is an EBike A constraint such as T extends Vehicle lets generic code use the interface and still return the caller’s own type.
06 / Try it
Can you explain a fresh example?
Decide whether each call compiles, and what it prints.
interface Vehicle {
name: string;
rangeKm(): number;
note?: string;
}
class EBike {
constructor(
public name: string,
public battery: number,
) {}
rangeKm(): number {
return this.battery / 2;
}
}
function dockLine(v: Vehicle): string {
return v.name + ": " + v.rangeKm() + " km";
}
const loan = { name: "loan bike", owner: "Ola", rangeKm: () => 5 };
console.log(dockLine(new EBike("e-bike", 60)));
console.log(dockLine(loan));
Does the program compile, and what does it print?
Reveal the reasoning
It compiles and prints e-bike: 30 km, then loan bike: 5 km. EBike never mentions Vehicle, but it has name and rangeKm, so it fits. loan has an extra property, owner, and no note. An object from a variable may have extra properties, and note is optional. Written directly in the call, the same object literal would fail the excess property check because of owner.
Practice tickets
- IFACE-101 · Named narrowboats show only a number on the hire board → Free
- IFACE-102 · Safari truck rides are charged at the wrong price → Sign in · Pro
- IFACE-103 · Voucher receipts show the full fare → Sign in · Pro