Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~21 min· topic 2 of 6

Topic 3.2

Interfaces

In one line

An interface describes the shape of an object: which properties it has, which are optional, which cannot be changed, and which methods it offers. It is a contract that any object can be checked against.

Think of it like this

A job advert. It says "must have: a name and an ID. Nice to have: a nickname. The ID can never change after hiring." Anyone who fits the advert can apply, and the advert doesn't care who they are. An interface is that advert for objects. It lists what an object must have, and the checker compares every object against it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Interface
A named description of an object's shape: its properties and methods. It is erased before the program runs.
Member
One entry inside an interface: a property or a method.
Property
A named piece of data on an object, like name: string.
Method
A property whose value is a function you call, like greet(): string.
Optional member
A member marked with ? that the object may leave out. Its type includes undefined.
readonly
A marker that forbids assigning to a property after the object is created. The check happens at compile time only.
Bivariant
Checked loosely in both directions: a function taking a more specific parameter is accepted. Method syntax works this way.
Contract
A promise about what an object looks like or can do that the checker enforces.

Step by step

01Describe the shape

List each member with its type. Everything is required unless you say otherwise.

An object that has all required members, with the right types, fits the interface. The annotation : User makes TypeScript check that.

user.tswhole filets
interface User {
  id: number;
  name: string;
}

const asha: User = { id: 1, name: "Asha" };

02Optional and readonly

nickname?: string may be left out. readonly id: number may be set once, in the object literal, and never again.

A missing required member and an assignment to a readonly member are two different errors, each pointing at the exact name.

user.tswhole filets
interface User {
  readonly id: number;
  name: string;
  nickname?: string;
}

const asha: User = { id: 1, name: "Asha" };
asha.name = "Asha K";   // fine
asha.id = 2;            // error TS2540: Cannot assign to 'id' because it is a read-only property.
const broken: User = { id: 3 };
// error TS2741: Property 'name' is missing in type '{ id: number; }' but required in type 'User'.

03Handle the optional member

An optional member has the type string | undefined, so you cannot call string methods on it directly. Use ?? (use the right side if the left is undefined or null) or an if check.

This is the checker protecting you from Cannot read properties of undefined.

use.tswhole filets
function show(u: User) {
  return u.nickname ?? u.name;           // fine
  // return u.nickname.toUpperCase();    // error TS18048: 'u.nickname' is possibly 'undefined'.
}

04Methods and function properties

Both declarations accept the same objects. The difference is how strictly the checker compares the function's parameters.

Use property syntax (handle: (e: Event) => void) when you want the strict check, which is what you want most of the time. Use method syntax when you describe a normal method of an object or class.

handlers.tswhole filets
interface WithMethod {
  handle(e: Event): void;            // method syntax: parameters compared loosely
}
interface WithProperty {
  handle: (e: Event) => void;        // property syntax: compared strictly
}

05Interfaces are contracts, then erased

The checker compares objects with the interface everywhere you use the name. Then everything is erased and the program runs as plain JavaScript objects.

Interfaces are contracts, then eraseddiagram
Rendering diagram…

Try it yourself

  1. 1

    Fix both errors

    In the first example, delete the line asha.id = 2; and give broken a name. Predict that the Types panel will show only the hover for asha.

  2. 2

    Allow undefined on purpose

    In the third example change nickname?: string; to nickname?: string | undefined;. Predict which error disappears.

    The TS2375 error goes away: you said undefined is an allowed value, not only an absent key.

  3. 3

    Switch the method to a property

    In the second example change handle(e: Event): void; in WithMethod to handle: (e: Event) => void;. Predict how many errors the Types panel shows now (before the change there was one).

    Now both lines are rejected: the loophole only exists for method syntax.

Code & diagrams

Required, optional and readonly

Two different errors: one for the readonly id, one for the missing name. The program still runs. Delete the two bad lines and the Types panel goes clean.

Console

(nothing was printed)

Expected console output

Asha K
Method or function property?

Both objects end up with a function called handle. Only the property version is rejected, because a function that needs a MouseEvent is not safe to call with any Event. Method syntax lets this through (a deliberate loophole, see Pro).

Console

(nothing was printed)

Expected console output

function function
Optional is not undefined, and readonly is shallow

With exactOptionalPropertyTypes (from tsc --init) a missing key and a key set to undefined are different, as the 'in' line proves at runtime. And readonly stopped the assignment but not the push.

Console

(nothing was printed)

Expected console output

{ name: 'Ravi' } { name: 'Ravi', nickname: undefined }
false true
[]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Change a readonly property

After const asha: User = { id: 1, name: "Asha" }; write asha.id = 2; where id is readonly.

terminal
$ Types panel
── what you'll see ──
index.ts(10,6): error TS2540: Cannot assign to 'id' because it is a read-only property.

Break #2

Forget a required member

Write const broken: User = { id: 3 }; when name is required.

terminal
$ Types panel
── what you'll see ──
index.ts(11,7): error TS2741: Property 'name' is missing in type '{ id: number; }' but required in type 'User'.

Myth vs fact

Myth

readonly makes an object frozen at runtime.

Fact

It is only a compile-time check on assignment through that name. It is erased, and it is shallow: a readonly property holding an array can still have items pushed.

Myth

An optional property is the same as a property that can be undefined.

Fact

Under exactOptionalPropertyTypes they differ: a key may be absent (?) or present with the value undefined only if you write | undefined explicitly.

Myth

An interface creates something I can use at runtime.

Fact

It is erased completely. For a value you can test with instanceof, you need a class.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Method syntax is bivariant on purpose. Without it, Array<Dog> would not be assignable to Array<Animal> because of methods like push, and a lot of everyday code would break. TypeScript chose convenience over soundness here: today const animals: Animal[] = dogs; animals.push({ name: "cat" }); type-checks even though dogs now holds a non-dog. And strictFunctionTypes (TypeScript 2.6) tightened only function-typed properties and plain function types.

  • ▸

    readonly on a property does not affect assignability (whether one type may be used where another is expected): { readonly id: number } and { id: number } are mutually assignable, so a readonly object can be handed to code that mutates it. Treat readonly as documentation the checker polices inside one scope.

  • ▸

    Interfaces can describe things beyond plain objects: callable ((x: number): string), constructable (new (x: number): Foo) and indexable ([key: string]: T, Topic 3.6). They can also be generic, interface Box<T> { value: T }, which you'll use constantly from Phase 7.

Remember this

  1. 1

    The problem this solves: without a written shape, a missing name or a typo like user.nmae only shows up when the program runs. With an interface, the checker finds it while you type. The syntax is interface Name { ... } with one member per line: interface User { id: number; name: string; }. A member is a property (a name with a value, like name: string) or a method (a name you can call, like greet(): string). Separate members with ; or a new line. Unlike type, there is no =.

  2. 2

    Add ? after a name to make it optional: nickname?: string. An object may leave it out. Reading it gives string | undefined, so TypeScript makes you handle the missing case before you use it as text. With exactOptionalPropertyTypes on (it comes with tsc --init and is on in these examples), "leave it out" and "set it to undefined on purpose" are different things: { nickname: undefined } is an error unless you write nickname?: string | undefined.

  3. 3

    Add readonly before a name and the property cannot be assigned to after the object is created: readonly id: number. Trying user.id = 2 is error TS2540. This is a compile-time promise only: nothing stops JavaScript code from changing the value at runtime. It is also shallow: readonly members: string[] stops you replacing the array, but you can still push into it (the third example shows this). Topic 5.1 covers deeper immutability.

  4. 4

    An interface can describe behaviour in two ways. Method syntax is greet(): string. Property syntax is greet: () => string. Both mean "has a function called greet", and objects fit either. They differ in one checking rule: for method syntax TypeScript compares parameters bivariantly (loosely, it accepts a function that takes a more specific parameter), but for property syntax with strictFunctionTypes (part of strict) it compares them strictly. The second example shows the difference with a handler that takes MouseEvent where Event is promised.

  5. 5

    The checker reports problems with the exact member: a missing required member is TS2741: Property 'name' is missing in type ... but required in type 'User', a wrong type is TS2322, an unknown extra name in a fresh object literal is TS2353. Reading these as "which member, which side is wrong" makes them easy to fix.

  6. 6

    An interface is only a description. It produces no JavaScript: the object you create is an ordinary object, and the interface is erased. If you need something that exists at runtime, such as an instanceof check, you need a class (Phase 6).

Explain it without notes

01

What is an interface, and what can it describe?

02

What is the difference between an optional property and a property whose type includes undefined?

03

What does readonly guarantee, and what does it not?

04

What is the difference between greet(): string and greet: () => string in an interface?

Practice

01

Write an interface Book with a required title (text), a required pages (number), an optional isbn (text) and a readonly id (number). Create one valid book, then try changing its id and write down the error code.

02

Write an interface Counter with a method increment(): number and a readonly property label. Make an object that satisfies it with a private count held in a closure and log two increments.

Trade-offs

  • ↔

    Optional members are convenient but spread undefined checks through the code. When a value is really required at some stage, make two interfaces (a draft and a saved version) instead of one with many optional members.

  • ↔

    readonly is cheap documentation but gives no runtime protection. If immutability matters for correctness, also use Object.freeze or immutable updates.

Done when you can

  • I can write an interface with required, optional and readonly members.

  • I can read TS2741 and TS2540 and say what to change.

  • I can handle an optional member with ?? or an if check.

  • I can explain why readonly is shallow and erased.

  • I can say what changes between method syntax and function-property syntax.