TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
218: Type-Safe Mocking Patterns
Imagine you're directing a big action movie. You have a lead actor who is great at delivering lines but probably shouldn't be jumping off a building. So, you hire a stunt double. For the scene to work, that double doesn't just need to "look" like the actor; they need to be able to perform every specific action the script requires. If the script says the character needs to slide across a hood of a car while firing a prop gun, and your stunt double can't handle a gun, the whole production grinds to a halt.
In TypeScript, your interfaces are the script. Your real services are the lead actors, and your mocks are the stunt doubles. The biggest mistake I see developers make is hiring a "stunt double" that is just a random object cast as the interface. They tell the compiler, "Trust me, this object is a PaymentGateway," even though it's missing half the methods. That's like hiring a stunt double who doesn't know how to jump and just hoping the camera doesn't catch it. When the "script" (your interface) changes, your mocks don't break—they just silently fail at runtime, and you're left hunting for a TypeError: x is not a function in your test suite.
Stop Lying to Your Compiler
Usually, when people are in a rush, they do something like this:
const mockGateway = {
processPayment: () => Promise.resolve({ success: true })
} as unknown as PaymentGateway;
I'll be honest: this is a trap. By using as unknown as T, you've effectively turned off TypeScript for this object. If you add a refundPayment method to the PaymentGateway interface tomorrow, this mock won't complain. Your tests will pass, but your code will crash in production. You've lied to the compiler, and the compiler can no longer protect you.
Building a Faithful Stunt Double
The right way to do this is to actually implement the interface. I know it feels like more boilerplate, but it's the only way to ensure your mocks evolve alongside your production code. If the interface changes, the mock breaks immediately at compile time. That's exactly what we want.
interface PaymentGateway {
processPayment(amount: number, currency: string): Promise<PaymentResult>;
getBalance(): Promise<number>;
}
class MockPaymentGateway implements PaymentGateway {
// We can use these to track calls in our tests
public processPaymentCalledWith: [number, string][] = [];
async processPayment(amount: number, currency: string): Promise<PaymentResult> {
this.processPaymentCalledWith.push([amount, currency]);
return { success: true, transactionId: 'mock-123' };
}
async getBalance(): Promise<number> {
return 100.00;
}
}
Now, the MockPaymentGateway is tethered to the PaymentGateway contract. If I add a new method to the interface, TypeScript will scream at me until I update the mock. This is the "type-safe" part of mocking.
Handling the "I Only Need One Method" Problem
Sometimes you're testing a small function that only uses one method of a massive interface. Implementing 20 methods just to test one is a slog. In these cases, I use a factory function combined with Partial<T>, but with a careful twist to maintain safety.
Instead of casting the whole object, create a helper that lets you specify only what you need, while still acknowledging the rest is missing. Or, better yet, use a mock library's built-in type helpers (like jest.Mocked<T>), but always ensure the base is derived from the actual interface.
// A helper to create a partial mock that is still typed
function createPaymentMock(overrides: Partial<PaymentGateway>): PaymentGateway {
return {
processPayment: async () => ({ success: false, transactionId: '' }),
getBalance: async () => 0,
...overrides,
} as PaymentGateway;
// This cast is safer because the factory provides defaults for all required methods
}
const myMock = createPaymentMock({
getBalance: async () => 500,
});
By providing defaults in the factory, you ensure that the object returned is a complete PaymentGateway, but you give yourself the flexibility to override only the behavior that matters for your current test case.
📋 Practical Task
Implementing a Type-Safe UserAnalytics Mock
You are working on a system that tracks user behavior. You have the following interface:
interface UserAnalytics {
trackEvent(eventName: string, properties: Record<string, any>): void;
identifyUser(userId: string, traits: Record<string, any>): void;
setUserName(name: string): void;
}
Your task is to create a MockUserAnalytics class that implements this interface. However, simply implementing the methods isn't enough for a good test—you need to be able to verify that the methods were called.
- Create a class
MockUserAnalyticsthat implements theUserAnalyticsinterface. - Add a public property
events: string[]to the class that stores theeventNameevery timetrackEventis called. - Ensure that if a new method is added to the
UserAnalyticsinterface, your mock class will cause a TypeScript compilation error. - Instantiate the mock and call
trackEvent('button_click', { color: 'red' }), then verify that theeventsarray contains'button_click'.
There are no comments for now.