TypeScript from the ground up

Objects and interfaces

Describing the shape of an object once — with an interface — and having every function that takes it, and every object that claims to be it, checked against that description.

Part 4 of 13

Beginner15 min readBack to Courses

Objects have shapes

In JavaScript an object is a bag of values, and nothing stops a typo from putting titel in the bag. In TypeScript you describe the shape once — the names and the types of the values — and give that description a name.

interface Book { title: string; author: string } is that description. It is not a class and it does not exist at run time; it is the shape written down, for the compiler to hold code against. Because it has no run-time presence it costs nothing — no object is created, no wrapper, no lookup. It is a description the way a comment is, except this one is checked.

One description, two places it keeps its promise

interface Book {
  title: string;
  author: string;
  year?: number;
}

function describe(book: Book): string {
  return book.title + " by " + book.author;
}

describe({ title: "Dune", author: "Herbert" });

The check runs both ways

A function that takes a Book may read book.title and book.author without wondering whether they are there. And an object passed as a Book must carry every required field with the right type — a missing author is caught at the call, where it was written.

A question mark marks a property the object may leave out: year?: number. Inside the function it is number | undefined, the same rule optional parameters follow. Read it only after saying what happens when it is missing.

Type aliases and interfaces

An object shape can also be written with type Book = { title: string; author: string }. For describing objects the two are interchangeable, and this course uses interface because the word announces what it is.

What a type can do and an interface cannot is describe things that are not objects — a union of strings is the one you will meet soon. The habit that keeps it simple: interface for shapes, type for everything else.

Structural typing, and the literal check

TypeScript compares shapes, not names. Any object with a title string and an author string is a Book, even if it was built somewhere that never mentions the interface. That is structural typing: if it has the right members, it fits.

A fresh object literal gets one extra check. Passing { title: "Dune", author: "Herbert", yearr: 1965 } is refused because yearr is not a property the shape knows — a typo caught at the line that wrote it. Assign the same literal to a variable first and the extra property is allowed, because the freshness check only applies to literals. Structural typing cares that the required members are present, not that nothing else is.

Shapes at the edge

An interface is a promise about values your program builds. It cannot check a value that arrived as JSON, because the annotation is erased before the data is read. At that border, an interface tells you what you hope for; a guard has to prove it.

Inside the program, the shape is enough. Every function that takes a Book can read its fields without a guard, because the only way to obtain a Book was to satisfy the shape.

What the checker does with a shape

When a value is assigned to a Book, the checker does not look for a constructor or a tag. It lists the members the interface requires, then asks for each one in turn: is there a title, and is it a string; is there an author, and is it a string. Extra members are ignored. The answer is yes or no, and the error names the first member that did not match.

This is why two unrelated interfaces with the same fields are interchangeable. A Book and a Novel that both carry title and author can be passed to the same function, because the function only ever asked for those two strings. The name is for you; the members are what the compiler checks.

Three mistakes this chapter is written against. An optional property read without a guard is undefined wearing a type — handle the missing case. An extra property on a fresh literal is refused, but the same object through a variable is not, so do not rely on freshness as validation. And mutating an object another function still holds is not a type error at all: the shape stays valid while the value changes under someone.

Try these next

  • Add an optional year and print a fallback when it is missing.
  • Pass an object with an extra field through a variable and then directly, and compare what the compiler says.
  • Give the same shape two names and decide which one the function should take.

Describe a book

Declare an interface for a book with a title and an author, both text, then type the parameter of `describe` with it.

Live Preview
Console output will appear here...

4 left to do

  • The book shape is declared
  • The title is typed as text
  • The author is typed as text
  • The function takes the declared shape

An interface lists properties one per line: title: string;. Once it has a name, the function parameter uses it like any other type: book: Book.

A book with a missing year

Add an optional `year` to the interface and print the title with the year, or with "year unknown" when it is absent.

Live Preview
Console output will appear here...

3 left to do

  • The year is optional
  • A missing year gets a fallback
  • The fallback says the year is unknown

Mark the property year?: number, then use book.year ?? "year unknown" — the nullish operator covers exactly the missing case.

Give a shape a name the moment it is used twice. Two anonymous object types that have to stay in step are one rename away from disagreeing; one shared name cannot drift from itself.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is an interface, at run time?