CodePark/ questions / Abstract classes, interfaces & structural typing
START PRACTICINGPRACTICE
Backend question · Object-oriented programming › Abstract classes, interfaces & structural typing

Why can you pass a plain object where a class type is expected in TypeScript? When does that stop working, and how does this help testing?

class Mailer {
  send(to: string, body: string): Promise<void> { /* SMTP */ }
}
const fake: Mailer = { send: async () => {} }; // compiles

A class name is also a type, and TypeScript checks types by shape (structural typing). The type Mailer means "an object with a send(to, body) method that returns a Promise". The object literal has that shape, so it fits. implements is optional for the same reason: it only asks the compiler to check the shape early.

When it stops working: as soon as the class has a private, protected or #private member. Then only real instances of that class (or its subclasses) match. TypeScript treats such a class almost like a named (nominal) type.

Why it helps testing:

  • You can pass { send: jest.fn() } to a service that expects a Mailer. No mocking library is needed.
  • Keep the types you depend on small. Then fakes stay small too.

The catch: a structural match is not a runtime check. fake instanceof Mailer is false. Code that uses instanceof will treat the fake differently.

  • A class name is also a type, and TypeScript compares types by shape.
  • Any object with the same public members fits the class type.
  • A private, protected or #private member makes only real instances match.
  • This lets tests pass small fake objects without a mocking library.
  • A fake that matches the type still fails instanceof.