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 includesundefined. - 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.
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.
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.
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.
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.
Try it yourself
- 1
Fix both errors
In the first example, delete the line
asha.id = 2;and givebrokenaname. Predict that the Types panel will show only the hover forasha. - 2
Allow undefined on purpose
In the third example change
nickname?: string;tonickname?: string | undefined;. Predict which error disappears.The
TS2375error goes away: you saidundefinedis an allowed value, not only an absent key. - 3
Switch the method to a property
In the second example change
handle(e: Event): void;inWithMethodtohandle: (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
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 KBoth 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 functionWith 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.
Break #2
Forget a required member
Write const broken: User = { id: 3 }; when name is required.
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 toArray<Animal>because of methods likepush, and a lot of everyday code would break. TypeScript chose convenience over soundness here: todayconst animals: Animal[] = dogs; animals.push({ name: "cat" });type-checks even thoughdogsnow holds a non-dog. AndstrictFunctionTypes(TypeScript 2.6) tightened only function-typed properties and plain function types. - ▸
readonlyon 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. Treatreadonlyas 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
The problem this solves: without a written shape, a missing
nameor a typo likeuser.nmaeonly shows up when the program runs. With an interface, the checker finds it while you type. The syntax isinterface Name { ... }with one member per line:interface User { id: number; name: string; }. A member is a property (a name with a value, likename: string) or a method (a name you can call, likegreet(): string). Separate members with;or a new line. Unliketype, there is no=. - 2
Add
?after a name to make it optional:nickname?: string. An object may leave it out. Reading it givesstring | undefined, so TypeScript makes you handle the missing case before you use it as text. WithexactOptionalPropertyTypeson (it comes withtsc --initand is on in these examples), "leave it out" and "set it toundefinedon purpose" are different things:{ nickname: undefined }is an error unless you writenickname?: string | undefined. - 3
Add
readonlybefore a name and the property cannot be assigned to after the object is created:readonly id: number. Tryinguser.id = 2is errorTS2540. 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 stillpushinto it (the third example shows this). Topic 5.1 covers deeper immutability. - 4
An interface can describe behaviour in two ways. Method syntax is
greet(): string. Property syntax isgreet: () => string. Both mean "has a function calledgreet", 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 withstrictFunctionTypes(part ofstrict) it compares them strictly. The second example shows the difference with a handler that takesMouseEventwhereEventis promised. - 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 isTS2322, an unknown extra name in a fresh object literal isTS2353. Reading these as "which member, which side is wrong" makes them easy to fix. - 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
instanceofcheck, you need a class (Phase 6).
Explain it without notes
What is an interface, and what can it describe?
What is the difference between an optional property and a property whose type includes undefined?
What does readonly guarantee, and what does it not?
What is the difference between greet(): string and greet: () => string in an interface?
Practice
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.
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
undefinedchecks 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. - ↔
readonlyis cheap documentation but gives no runtime protection. If immutability matters for correctness, also useObject.freezeor immutable updates.
Done when you can
I can write an interface with required, optional and
readonlymembers.I can read
TS2741andTS2540and say what to change.I can handle an optional member with
??or anifcheck.I can explain why
readonlyis shallow and erased.I can say what changes between method syntax and function-property syntax.