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
146: Extending Types with Intersections
I see this happen all the time when developers move from JavaScript to TypeScript: they treat the intersection operator (&) like the object spread operator (...). In JS, if you spread two objects and they both have a role property, the second one simply wins. It overwrites the first. Many learners assume TypeScript's & works the same way—as a mechanism for "overriding" a type definition.
Intersections aren't Object Spreads
Let's look at why that assumption will break your build. Imagine you have a basic User type and you want to create a SuperUser where the accessLevel is a specific number instead of a generic string.
type User = {
name: string;
accessLevel: string;
};
type SuperUser = User & {
accessLevel: number;
};
const admin: SuperUser = {
name: "Alice",
accessLevel: 10 // Error!
};
You might expect accessLevel to now be a number. But TypeScript is screaming at you. Why? Because an intersection is a logical AND. You are telling TypeScript: "A SuperUser must be a User AND it must have an accessLevel that is a number."
Since a value cannot be both a string and a number simultaneously, TypeScript evaluates string & number as never. You've accidentally created a type that is impossible to instantiate. This is a classic trap. Intersections are for combining requirements, not for replacing them.
Combining Distinct Capabilities
The real power of intersections comes when you're composing types based on "capabilities" or "traits." I find this incredibly useful when building plugins or complex configurations where an object needs to satisfy multiple different interfaces.
Think about a logging system. You might have one type for things that can be logged to a file, and another for things that can be sent to a remote API. Instead of creating one massive, bloated interface, you can keep them lean and intersect them only when needed.
type Loggable = {
toLogString(): string;
};
type RemoteSyncable = {
endpoint: string;
sync(): void;
};
// This type requires BOTH capabilities
type CloudLogger = Loggable & RemoteSyncable;
const myLogger: CloudLogger = {
endpoint: "https://api.logs.io",
toLogString() {
return "System Heartbeat: OK";
},
sync() {
console.log("Syncing to cloud...");
}
};
In this scenario, CloudLogger isn't trying to change what Loggable is; it's just insisting that any object assigned to it must satisfy the contracts of both types. It's a way of saying, "I don't care what this object is fundamentally, as long as it can do X and it can also do Y."
I generally prefer this approach over deep inheritance chains. It keeps your types modular. If you later decide you need a LocalLogger, you just use Loggable & FileSystemWriter, and you haven't had to rewrite your core Loggable logic.
📋 Practical Task
Implementing a Multi-Auth User Session
You are building a session management system. You have a base Session type that tracks the sessionId and expiry. You also have an OAuthSession type that tracks the provider (e.g., "Google" or "GitHub") and an AdminSession type that tracks the adminLevel.
Your task is to create a new type called SecureAdminSession. This type must be an intersection of all three: Session, OAuthSession, and AdminSession. Once you've defined the type, create a constant named currentSession that conforms to SecureAdminSession with the following values:
sessionId: "sess_99"expiry: 1715000000provider: "GitHub"adminLevel: 5
type Session = {
sessionId: string;
expiry: number;
};
type OAuthSession = {
provider: string;
};
type AdminSession = {
adminLevel: number;
};
// Your code here:
There are no comments for now.