InteractiveFrameworks

Interfaces

Describe the shape of an object: required, optional and readonly properties, one interface extending another, methods an object may have, and records with fixed keys. Then see that TypeScript compares shapes, not names.

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

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.

  1. Animal's id never changes after the animal is created.
  2. Cat extends Animal, must say whether it lives indoor, and may have a microchip number.
  3. Shelter may have an onAdopt method, called with each cat that leaves.
  4. A Census has exactly two keys, indoor and outdoor, each a number.
  5. census(cats) counts the cats by where they live, and accepts a readonly list.
  6. describeCat(cat) answers "Tom, 3, indoor, chip 981000", or "Luna, 5, outdoor, no chip" without a microchip.
  7. adopt(shelter, id) takes that cat out of the shelter, calls onAdopt with it when the shelter has one, and returns it; for an unknown id it returns undefined and 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 instance Record<string, number> where two keys were asked for.
  • The type 'readonly Cat[]' is 'readonly' and cannot be assigned to the mutable type 'Cat[]': census still takes a mutable array.
  • "tells the shelter who left" fails: onAdopt is not called, or is called even for an unknown id.

Remember

  • ? makes a property optional, readonly makes it unassignable, extends builds on another interface.
  • Optional methods are called with ?.().
  • Types are compared by shape; only object literals are held to exactly the listed properties.
  • Record with 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.