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 aMailer. 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
#privatemember 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.