Files
What you'll learn
- Declare an interface with required, optional and readonly properties, and extend it
- Describe an optional method and call it only when it is there
- Explain structural typing: any value with the right shape fits, whatever it is called
Interfaces
Most of what a backend passes around is objects: a request body, a row, a service's answer. Describing their shape once, and having every function agree on it, is what stops a rename in one place from silently breaking ten others. An interface is that description: which properties an object has, which it may have, which never change, and which methods it offers. NestJS leans on them everywhere, from the options a module accepts to the CanActivate a guard implements, so reading and writing them fluently comes first.
Properties: required, optional, readonly
interface Owner {
readonly id: number; // set once, never assigned again
name: string; // required
phone?: string; // optional: string | undefined
}
A missing required property is a compile error where the object is created. An optional one is undefined when absent, and the type says so, so code that reads it has to handle that case. readonly forbids assignment after creation: owner.id = 2 does not compile. It is a compile-time promise, not a freeze at runtime.
Extending
An interface can build on another. Everything in the parent is required in the child too, and a child is accepted anywhere the parent is:
interface Person { name: string }
interface Owner extends Person { readonly id: number; phone?: string }
function greet(person: Person) { return `Hello, ${person.name}`; }
greet(someOwner); // fine: an Owner is a Person
Methods, and optional ones
An interface can require methods, notify(message: string): void, or offer optional ones with ?. An optional method must be checked before it is called, and the optional call operator does both at once:
interface Mailer { send(to: string): void; onError?(error: Error): void }
mailer.onError?.(new Error('bounced')); // calls it if it exists, does nothing otherwise
Shapes, not names
TypeScript is structurally typed: a value fits an interface if it has the right properties, whatever it is called and wherever it came from. An object fresh from a database with an extra createdAt column is an acceptable Owner if it has an id and a name. Nest relies on this: a test can hand a service any object with the right methods instead of the real repository.
There is one exception, and it is deliberate. An object literal written straight into a typed place may not carry properties the type does not have: greet({ name: 'Ann', nmae: 'x' }) is an error, because a misspelt property there is almost always a mistake. A variable holding the same object passes.
Records with fixed keys
Record<K, V> is an object type whose keys are K and values V. With a union of literals as K, the object must have exactly those keys:
type Totals = Record<'paid' | 'unpaid', number>; // { paid: number; unpaid: number }
Record<string, number> would accept any key, and so would catch nothing.
readonly arrays
A function that only reads a list should say so: readonly Owner[] promises not to push, splice or sort it in place. The caller can then pass either a normal array or a readonly one. A parameter typed Owner[] refuses a readonly array, because it might change it.
interface or type?
type Owner = { … } describes an object just as well, and this site uses both. Interfaces can be extended with extends and reopened to add members; type aliases can also name unions and other types that are not objects. For object shapes, pick one convention and keep it.
Your task
The work is in shelter.ts; the spec's type checks tell you which types still need changing.
Animal'sidnever changes after the animal is created.CatextendsAnimal, must say whether it livesindoor, and may have amicrochipnumber.Sheltermay have anonAdoptmethod, called with each cat that leaves.- A
Censushas exactly two keys,indoorandoutdoor, each a number. census(cats)counts the cats by where they live, and accepts a readonly list.describeCat(cat)answers"Tom, 3, indoor, chip 981000", or"Luna, 5, outdoor, no chip"without a microchip.adopt(shelter, id)takes that cat out of the shelter, callsonAdoptwith it when the shelter has one, and returns it; for an unknown id it returnsundefinedand changes nothing.
When it fails
Unused '@ts-expect-error' directive: something the spec expects to be refused is still allowed. The comment beside it says what.Type 'false' does not satisfy the constraint 'true': a type is not exactly what the check names, for instanceRecord<string, number>where two keys were asked for.The type 'readonly Cat[]' is 'readonly' and cannot be assigned to the mutable type 'Cat[]':censusstill takes a mutable array.- "tells the shelter who left" fails:
onAdoptis not called, or is called even for an unknown id.
Remember
?makes a property optional,readonlymakes it unassignable,extendsbuilds on another interface.- Optional methods are called with
?.(). - Types are compared by shape; only object literals are held to exactly the listed properties.
Recordwith literal keys fixes the keys;readonly T[]promises not to change a list.
Stuck? Show a hint
readonly before a property forbids assigning it later; ? after a name makes it optional. interface Cat extends Animal { … } adds to Animal. An optional method is onAdopt?(cat: Cat): void, and shelter.onAdopt?.(cat) calls it only if it exists. Record<'indoor' | 'outdoor', number> has exactly those keys. A parameter of type readonly Cat[] accepts both kinds of array.
Press Run tests to start the app. Its log appears here.Tests
- shelter.spec.tsrun to see its tests