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
18: Interfaces
You've probably seen type and interface used interchangeably in a dozen tutorials, and honestly, for about 90% of your daily work, they'll behave exactly the same. This leads to a very common misconception: that an interface is just a slightly different syntax for a type alias. If you think of them as the same thing, you're missing the actual architectural purpose of the interface.
The "Interface is just a Type Alias" Identity Crisis
If you define a type User = { name: string } and an interface User { name: string }, they both describe the same object shape. But here is where that logic falls apart: types are closed, while interfaces are open.
Take a look at this. I'm pretending I'm using a third-party library that defines a Window object. If that library used a type, I'd be stuck with whatever they gave me. But because it's an interface, I can do this:
// Imagine this is inside a library you didn't write
interface Window {
title: string;
}
// In your own code, you can just... add to it.
interface Window {
customPluginId: string;
}
const myWindow: Window = {
title: "My App",
customPluginId: "abc-123"
};
This is called declaration merging. TypeScript sees two interfaces with the same name in the same namespace and merges them into one. You cannot do this with types. If you try to declare type Window twice, the compiler will scream at you. This is why interfaces are the gold standard for library authors and large-scale architectures; they allow others to extend the "contract" without modifying the original source code.
Defining a Contract, Not Just a Shape
I like to think of interfaces as a contract. When I use an interface, I'm not just saying "this object has these fields"; I'm saying "any class or object that claims to be a PaymentProcessor must fulfill these requirements."
This becomes powerful when you use the implements keyword with classes. It forces the class to adhere to the interface, which is a lifesaver when you're working in a team and want to ensure that different developers are building compatible modules.
interface Logger {
log(message: string): void;
error(message: string): void;
}
// This class is now contractually obligated to have log() and error()
class CloudLogger implements Logger {
log(message: string) {
console.log(`Sending to cloud: ${message}`);
}
error(message: string) {
console.error(`Cloud Alert: ${message}`);
}
}
If I forget to implement error() in the CloudLogger class, TypeScript will flag it immediately. I don't have to wait until runtime to find out that a method is missing.
Read-only and Optionality in Contracts
Since interfaces are often used for data coming from APIs or configuration files, you'll frequently use the readonly and ? modifiers. I use readonly religiously for things that should never change after the object is created—like a user's unique ID.
interface APIResponse {
readonly status: number; // Cannot be changed after initialization
data: any;
errorMessage?: string; // Optional: might not be there if the request succeeded
}
By marking status as readonly, I prevent some future version of myself (or a teammate) from accidentally overwriting the response code halfway through a function. It turns a potential runtime bug into a compile-time error.
📋 Practical Task
Building a Plugin-Based Notification System
You are building a notification system that needs to support different channels (Email, SMS, Slack). You need to ensure that every new channel added to the system follows the exact same structure so the main NotificationManager can use them interchangeably.
Your Task:
- Create an interface named
NotificationChannelthat requires asend(message: string): booleanmethod and areadonly channelName: stringproperty. - Create a class
EmailChannelthat implementsNotificationChannel. In thesendmethod, log "Sending Email: [message]" and returntrue. - Create a class
SmsChannelthat implementsNotificationChannel. In thesendmethod, log "Sending SMS: [message]" and returntrue. - Write a function
dispatchNotification(channel: NotificationChannel, msg: string)that calls thesendmethod of the provided channel. - Instantiate both classes and pass them into
dispatchNotificationto verify they work.
There are no comments for now.