TypeScript lesson 4 of 9
Interfaces and Object Types
How to name the shape of an object with interface, what structural typing means and why it lets unrelated interfaces stand in for each other, and how optional and readonly properties change what the compiler allows.
Published · Every example on this page was run before it was published.
An object literal in JavaScript has no fixed shape - any property can be added, left off, or spelled
wrong, and nothing complains until something later tries to read a property that was never there.
TypeScript's interface gives an object shape a name once, so every place that shape is used - a
variable, a function parameter, another object - can be checked against it.
Defining an Interface
An interface lists property names and the type each one must hold:
interface Product {
name: string;
price: number;
inStock: boolean;
}
const pen: Product = { name: "Pen", price: 1.5, inStock: true };
console.log(pen);{ name: 'Pen', price: 1.5, inStock: true }Every property listed in Product is required by default, and the object literal assigned to pen
must have all three, with matching types, in order for the assignment to type-check. Leave one out and
the compiler stops you before anything runs:
interface Ticket {
event: string;
price: number;
}
const ticket: Ticket = { event: "Concert" };main.ts(6,7): error TS2741: Property 'price' is missing in type '{ event: string; }' but required in type 'Ticket'.Structural Typing
TypeScript does not check which interface a value was declared against - it checks whether the value's shape matches. This is called structural typing: two interfaces with identical properties are interchangeable, even if they were never connected to each other in any way.
interface Point2D {
x: number;
y: number;
}
interface Coordinate {
x: number;
y: number;
}
function distanceFromOrigin(p: Point2D): number {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
const location: Coordinate = { x: 3, y: 4 };
console.log(distanceFromOrigin(location));5distanceFromOrigin expects a Point2D, and location was declared as a Coordinate - a completely
separate interface, with no extends or shared name linking the two. TypeScript accepts the call
anyway, because both interfaces describe exactly the same shape: an object with numeric x and y
properties. What a value is matters to TypeScript; what it was called when it was declared does not.
Optional Properties
A property name followed by ? may be left out of the object entirely:
interface Product {
name: string;
price: number;
discount?: number;
}
function finalPrice(item: Product): number {
if (item.discount) {
return item.price - item.discount;
}
return item.price;
}
const pen: Product = { name: "Pen", price: 2 };
const notebook: Product = { name: "Notebook", price: 5, discount: 1 };
console.log(finalPrice(pen));
console.log(finalPrice(notebook));2
4discount does not need to appear in pen at all - because it is marked optional, TypeScript treats
its type inside the function as number | undefined, which is exactly why finalPrice checks
if (item.discount) before using it as a number.
readonly Properties
A property marked readonly can be set when the object is first created, but never reassigned
afterward:
interface Config {
readonly apiVersion: string;
timeout: number;
}
const settings: Config = { apiVersion: "v2", timeout: 3000 };
settings.apiVersion = "v3";main.ts(7,10): error TS2540: Cannot assign to 'apiVersion' because it is a read-only property.timeout has no such restriction, so settings.timeout = 5000 on its own line would compile without
issue - readonly applies per property, not to the whole object.
Type Aliases: interface's Cousin
A type alias can describe an object shape too, using = instead of a block:
type ProductAlias = {
name: string;
price: number;
};For a plain object shape like this one, interface and type are close to interchangeable - structural
typing applies exactly the same way regardless of which one was used to describe the shape. The two do
diverge for more advanced uses: an interface can be extended with extends and reopened later in the
same file to add more properties, while a type alias can combine shapes with | and & the way you
will see in a later lesson on union types - but for a single, static shape, either one works.
Common Mistakes
Adding a property the interface never declared. TypeScript checks object literals assigned directly to a typed variable for properties that should not be there, not only ones that are missing:
interface Ticket {
event: string;
price: number;
}
const ticket: Ticket = { event: "Concert", price: 40, vip: true };main.ts(6,55): error TS2353: Object literal may only specify known properties, and 'vip' does not exist in type 'Ticket'.This check - called an excess property check - only runs on object literals written directly at the assignment. Building the same object in a variable first and assigning that variable instead would skip it, which is worth knowing but not something to rely on; an extra property is usually a sign of a typo or a stale field.
Assuming readonly makes the whole object immutable. readonly only blocks reassigning the
property itself. If that property holds an array or another object, its contents can still be changed
freely, because nothing about that inner value's own mutability was declared:
interface Cart {
readonly items: string[];
}
const cart: Cart = { items: ["pen"] };
cart.items.push("notebook");
console.log(cart.items);[ 'pen', 'notebook' ]cart.items = [] would be rejected, because that reassigns the readonly property. cart.items.push(...)
compiles fine, because it mutates the array items points at without ever reassigning items itself.
Treating a missing required property as something you can add later. The excess and missing
property checks shown above both fire at the moment of assignment - by the time your program is running,
every value already matches its declared shape exactly, so there is no later point where a required
property is still "on its way." If a property genuinely will not be known right away, marking it
optional with ? is the honest way to say that, and the code that reads it should check for undefined
the same way finalPrice checked for discount above.
Next Steps
The two-number-sum practice problem does not require an interface on its own, but writing a small
interface to describe the shape of a result you return from a helper function - an index pair, say -
is a natural way to practice this lesson while working through it.
In the TypeScript playground, define an interface for something with three or four properties, including at least one optional one, then build a matching object and deliberately misspell one property name in the literal. Read the "does not exist in type" message closely - it names the interface and the exact property it could not find a match for.
When you want a harder one
Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.