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
71: Mocking Typed Dependencies
When you're writing tests for a service that depends on other classes or APIs, you'll quickly hit a wall where TypeScript starts complaining that your mock objects aren't "compatible" with the interfaces they're replacing. It's frustrating because, in a test, you usually only care about one or two methods, not the entire API surface.
How do I mock an interface without implementing every single method?
If you have a massive interface—say, a DatabaseClient with 50 methods—you definitely don't want to manually write 50 dummy functions just to satisfy the compiler. The trick I use is to create a mock object and cast it using jest.Mocked<T> (or ts-jest's utilities).
Let's say we have a PaymentGateway interface. We only care about charge() for our current test, but the interface has a dozen other methods for refunds, disputes, and reporting.
interface PaymentGateway {
charge(amount: number, currency: string): Promise<string>;
refund(transactionId: string): Promise<void>;
getAccountBalance(): Promise<number>;
// ... 10 other methods
}
class CheckoutService {
constructor(private gateway: PaymentGateway) {}
async processOrder(amount: number) {
return await this.gateway.charge(amount, 'USD');
}
}
Instead of implementing the whole thing, you can do this:
const mockGateway = {
charge: jest.fn(),
} as unknown as jest.Mocked<PaymentGateway>;
const service = new CheckoutService(mockGateway);
mockGateway.charge.mockResolvedValue('tx_123');
await service.processOrder(100);
expect(mockGateway.charge).toHaveBeenCalledWith(100, 'USD');
By casting to unknown first and then to jest.Mocked<PaymentGateway>, you tell TypeScript: "Trust me, this object will behave like the interface, and I want the mock-specific methods (like mockResolvedValue) available on its properties."
Why should I use jest.mocked() instead of just casting to 'any'?
I see a lot of developers just use as any to get the tests to pass. Don't do that. Using any essentially turns off the compiler for that object. If you rename a method in your interface or change a parameter from a string to a number, your any cast will keep the tests "green" even though the code is now broken.
Using jest.mocked() (the helper function) keeps the type safety intact. It wraps the mock and ensures that the arguments you pass to mockReturnValue or mockResolvedValue actually match the types defined in the original interface.
import { mocked } from 'ts-jest'; // or use the built-in jest.mocked in newer versions
const mockGateway = {
charge: jest.fn(),
} as unknown as PaymentGateway;
const typedGateway = jest.mocked(mockGateway);
// TypeScript will error here if charge() is supposed to return a string
typedGateway.charge.mockResolvedValue(404); // Error: Argument of type 'number' is not assignable to 'string'
What if the dependency is a concrete class instead of an interface?
Mocking a class is slightly different because you're dealing with a constructor. If you're using jest.mock('./path-to-class'), Jest automatically replaces the class with a mock. However, TypeScript doesn't automatically know that the imported class is now a mock.
I usually handle this by importing the class and then casting the instance. If you're mocking a third-party library (like axios or a database driver), the pattern looks like this:
import { ExternalLogger } from './logger';
jest.mock('./logger');
const loggerInstance = new ExternalLogger();
const mockedLogger = jest.mocked(loggerInstance);
mockedLogger.log.mockImplementation(() => {
console.log('Mock logger called!');
});
One thing to watch out for: if the class has complex internal state or private methods that the service relies on, you might find it easier to extract an interface for that class. I've spent way too many hours fighting class prototypes when a simple interface would have solved the problem in five minutes.
📋 Practical Task
Exercise: Mocking the NotificationDispatcher
You are working on a UserRegistrationService that depends on a NotificationDispatcher. The dispatcher has a massive interface with methods for SMS, Email, Push notifications, and Slack integration. You need to test that when a user registers, the sendWelcomeEmail method is called with the correct email address.
Your Task:
- Create a mock for the
NotificationDispatcherinterface without implementing all its methods. - Ensure the mock is properly typed so you can use
mockResolvedValue. - Write a test case that verifies
UserRegistrationService.registercallssendWelcomeEmailexactly once with the provided email.
// The provided code to work with:
interface NotificationDispatcher {
sendWelcomeEmail(email: string): Promise<boolean>;
sendSms(phone: string, msg: string): Promise<void>;
sendPushNotification(userId: string, msg: string): Promise<void>;
sendSlackAlert(channel: string, msg: string): Promise<void>;
verifyRecipient(email: string): Promise<boolean>;
}
class UserRegistrationService {
constructor(private dispatcher: NotificationDispatcher) {}
async register(email: string) {
// Registration logic...
return await this.dispatcher.sendWelcomeEmail(email);
}
}There are no comments for now.