interface or type
Use interface for an object that something implements or extends, and type for everything else — a union, a function type, a mapped type. Being consistent matters more than which you pick.
interface Door {
name: string;
days: number;
}
type Door = {
name: string;
days: number;
};
interface | type | |
|---|---|---|
| Object shapes | yes | yes |
| Unions, tuples, primitives | no | yes |
| Extending | extends | & |
| Reopening to add fields | yes — declaration merging | no |
| In error messages | Shows the name | Often expanded |
Optional, readonly, and the difference from undefined
interface Door {
name: string;
note?: string; // may be absent
price: number | undefined; // must be present, may be undefined
readonly id: string; // cannot be reassigned after creation
}
const door: Door = { name: "Painted", price: undefined, id: "a" };
door.id = "b"; // Cannot assign to 'id' because it is a read-only property
readonly items: string[] stops you replacing the array, not pushing to it — use readonly string[] for that. And nothing is frozen at runtime.
Excess property checks
A literal assigned directly is checked for surplus keys, because a key that is not in the type is nearly always a typo. Assign it to a variable first and the check does not apply, which is either a useful escape hatch or a trap, depending on the day.
interface Door { name: string; days: number }
const extra = { name: "Painted", days: 5, colour: "sage" };
const a: Door = extra; // fine — it is a variable
const b: Door = { name: "Painted", days: 5, colour: "sage" };
// ^ Object literal may only specify known properties
Extending and combining
interface Product { id: string; price: number }
interface Door extends Product { name: string; days: number }
type Product = { id: string; price: number };
type Door = Product & { name: string; days: number };
// and a union is the other direction entirely
type Item = Door | Frame;
Index signatures and Record
interface Prices { [finish: string]: number }
const prices: Prices = { painted: 480, oiled: 520 };
type Prices = Record<string, number>; // the same thing
type ByFinish = Record<Finish, number>; // every finish required
prices.anything; // number — even though it is not there
prices.nonsense is typed number and is actually undefined. noUncheckedIndexedAccess fixes this by making every lookup number | undefined, which is correct and occasionally tiresome.
Nested and optional chains
interface Order {
id: string;
customer: { name: string; email?: string };
items: Array<{ door: Door; quantity: number }>;
placedAt: Date;
}
order.customer.email?.toLowerCase(); // optional chaining follows the type
Functions and methods in a type
Under strictFunctionTypes the property form is checked more strictly than the method shorthand — a historical compromise for the DOM. Prefer the property form in your own code.
interface Store {
add(door: Door): void; // method shorthand
remove: (id: string) => void; // property with a function type
readonly size: number;
}
Describing what you already have
That last pair is worth memorising: one array, used at runtime for the list and at compile time for the type, with no chance of the two drifting apart.
const defaults = { finish: "painted", days: 5, express: false };
type Defaults = typeof defaults; // { finish: string; days: number; express: boolean }
type Finish = typeof defaults.finish; // string
const FINISHES = ["painted", "oiled", "glazed"] as const;
type Finish = typeof FINISHES[number]; // "painted" | "oiled" | "glazed"