typescript / expert
Snippet
Type-Safe Test Doubles with the satisfies Operator
The 'satisfies' operator (TS 4.9+) is the right tool for building test doubles. Unlike a type annotation (': UserRepo'), it does not widen the variable to the interface — so findById keeps its narrower non-nullable return type, letting downstream code call .name.toUpperCase() without a null check. Unlike 'as UserRepo', it still verifies that the object fully conforms to the contract; forgetting a method or returning the wrong shape errors at compile time. For test doubles this means you get full IntelliSense on extra helper fields (call recorders, spies) while still being protected against drift when the production interface changes.
snippet.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface UserRepo {findById(id: string): Promise<{ id: string; name: string } | null>;save(user: { id: string; name: string }): Promise<void>;}const calls: string[] = [];const mockRepo = {findById: async (id: string) => ({ id, name: "Stub" }),save: async (u: { id: string; name: string }) => {calls.push(u.id);},} satisfies UserRepo;// 'satisfies' verifies conformance without widening the value:const user = await mockRepo.findById("u1");console.log(user.name.toUpperCase()); // name stays string, not string | null
Breakdown
1
} satisfies UserRepo;
Validates conformance to UserRepo but preserves the literal, narrower inferred type of the object.
2
findById: async (id: string) => ({ id, name: "Stub" }),
Inferred return is Promise<{ id: string; name: string }> — narrower than the interface's nullable union.
3
const user = await mockRepo.findById("u1");
Because the inferred type isn't widened, 'user' is non-nullable here — no defensive null check needed.
4
console.log(user.name.toUpperCase());
Compiles cleanly; with ': UserRepo' instead of satisfies, this would require narrowing.