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.
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