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
147: Class Implements Clauses
I've seen this happen in a dozen different projects: a developer creates a few classes that do similar things, and they assume that because the classes "look" the same, they can be used interchangeably. It works fine until the project grows, a new class is added, and suddenly you're hitting TypeError: x is not a function in production.
Take a look at this logging setup. It seems harmless enough at first glance.
class ConsoleLogger {
log(message: string) {
console.log(`[Console]: ${message}`);
}
}
class FileLogger {
log(message: string) {
// Imagine logic here to write to a file
console.log(`[File]: ${message}`);
}
}
function processData(logger: ConsoleLogger | FileLogger) {
logger.log("Processing started...");
// Wait, I remember FileLogger has a 'saveToDisk' method,
// but does ConsoleLogger? Let's try to use a specialized method.
(logger as any).flush();
}
const myLogger = new ConsoleLogger();
processData(myLogger);
The fragility of "Implicit" contracts
The problem here isn't just the as any (though that's a red flag). The real issue is that ConsoleLogger and FileLogger have no formal agreement. They both happen to have a log method, but there's nothing stopping someone from renaming log to write in FileLogger, or forgetting to add a required method to a new CloudLogger class.
When we rely on classes just "happening" to have the same methods, we're playing a dangerous game. We're relying on structural similarity without any enforcement. If you're building a system where different classes need to be swapped out—like switching from a mock database to a real one—you need a contract.
Enforcing the contract with implements
This is where the implements clause comes in. Instead of hoping our classes match, we define an interface that describes exactly what a "Logger" must be able to do. Then, we tell the class to implement that interface.
interface Logger {
log(message: string): void;
flush(): void;
}
class ConsoleLogger implements Logger {
log(message: string) {
console.log(`[Console]: ${message}`);
}
flush() {
console.log("Flushing console buffer...");
}
}
class FileLogger implements Logger {
log(message: string) {
console.log(`[File]: ${message}`);
}
flush() {
console.log("Writing remaining logs to disk...");
}
}
function processData(logger: Logger) {
logger.log("Processing started...");
logger.flush(); // Now this is type-safe!
}
Notice the shift. By adding implements Logger, we've turned a "suggestion" into a requirement. If I try to create a CloudLogger and forget the flush method, TypeScript will scream at me immediately. I don't have to wait for a runtime crash to realize I missed a method.
One thing to keep in mind: implements only checks the public side of your class. It doesn't care about your private methods or internal logic; it only cares that the external API matches the interface. It's also important to remember that implements doesn't actually provide any code. Unlike extends, which inherits behavior, implements is purely a check. You still have to write the actual method bodies in every class.
📋 Practical Task
Building a Pluggable Payment Gateway
You are building an e-commerce system that needs to support multiple payment providers (e.g., Stripe and PayPal). Currently, the code is inconsistent, and the main checkout function is crashing because some providers are missing the refund method.
Your Task:
- Create an interface called
PaymentGatewaythat requires two methods:processPayment(amount: number): booleanandrefundPayment(transactionId: string): boolean. - Create a class
StripeGatewaythat implementsPaymentGateway. - Create a class
PayPalGatewaythat implementsPaymentGateway. - Write a function
executeTransaction(gateway: PaymentGateway, amount: number)that callsprocessPayment. - Ensure that if you intentionally remove one of the required methods from either class, TypeScript flags it as an error.
There are no comments for now.