Skip to content

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:

TypeScript
interface Product {
  name: string;
  price: number;
  inStock: boolean;
}

const pen: Product = { name: "Pen", price: 1.5, inStock: true };
console.log(pen);
Output
{ 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:

TypeScript
interface Ticket {
  event: string;
  price: number;
}

const ticket: Ticket = { event: "Concert" };
Output
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.

TypeScript
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));
Output
5

distanceFromOrigin 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:

TypeScript
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));
Output
2
4

discount 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:

TypeScript
interface Config {
  readonly apiVersion: string;
  timeout: number;
}

const settings: Config = { apiVersion: "v2", timeout: 3000 };
settings.apiVersion = "v3";
Output
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:

TypeScript
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:

TypeScript
interface Ticket {
  event: string;
  price: number;
}

const ticket: Ticket = { event: "Concert", price: 40, vip: true };
Output
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:

TypeScript
interface Cart {
  readonly items: string[];
}

const cart: Cart = { items: ["pen"] };
cart.items.push("notebook");
console.log(cart.items);
Output
[ '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.