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
6: Understanding Structural Typing
I've noticed that developers coming from languages like Java or C# often hit a wall when they first encounter how TypeScript handles types. They're used to "Nominal Typing," where if you say a function needs a User object, you must pass it something that was explicitly declared as a User. TypeScript doesn't work like that. It uses structural typing.
Think of it like a USB-C port on your laptop. Your laptop doesn't care if the cable you plug in is branded by Apple, Samsung, or some random company from Amazon. It doesn't ask the cable for its "birth certificate" or "identity papers." It only cares about one thing: Does the plug fit the hole? If the physical shape (the structure) matches the port, the connection works. The identity of the cable is irrelevant; only its capabilities matter.
The "If it fits, it sits" Logic
In TypeScript, the "shape" of your data is what defines its type. If you define a type that requires a name and an email, any object that has at least those two properties will satisfy that type, regardless of where that object came from or what it's called.
interface Person {
name: string;
email: string;
}
function sendWelcomeEmail(user: Person) {
console.log(`Sending mail to ${user.name} at ${user.email}`);
}
// This is a 'Customer', not a 'Person'
const customer = {
name: "Alice",
email: "alice@example.com",
customerId: 12345, // Extra property
loyaltyPoints: 500 // Extra property
};
// This works perfectly!
sendWelcomeEmail(customer);
In a nominal system, the line sendWelcomeEmail(customer) would throw a compiler error because customer wasn't explicitly labeled as a Person. But in TypeScript, the compiler looks at customer and says, "Well, it has a name (string) and an email (string). That's all I asked for. I don't care that it also has a customerId. It fits the port."
Why Extra Properties Are Usually Fine
You might wonder why TypeScript allows customer to have extra properties like loyaltyPoints. The reason is flexibility. In real-world software, your data often evolves. You might have a base set of requirements for a function, but the objects flowing through your system are often richer than what a single function needs to know about.
This makes your code much more decoupled. I can write a Logger that only needs an object with a message property, and I can pass it a full Error object, a Request object, or a simple string-wrapper without having to write complex inheritance hierarchies just to make the types match.
The One Catch: Fresh Object Literals
Here is a quirk that trips people up: TypeScript is more strict when you pass an object literal directly into a function. I call this "Excess Property Checking."
// This will throw a TS error!
sendWelcomeEmail({
name: "Bob",
email: "bob@example.com",
phoneNumber: "555-0123" // Error: 'phoneNumber' does not exist in type 'Person'
});
Wait, why did the customer object work, but this literal doesn't? It's because when you pass a literal, TypeScript assumes you're making a mistake. It figures that if you're typing out a brand new object right there in the function call, and you're adding properties that the function doesn't use, you probably have a typo or you're passing the wrong object entirely. It's a safety feature to prevent you from misspelling email as emall and wondering why the value is undefined.
Once that object is assigned to a variable (like we did with customer), TypeScript relaxes and goes back to pure structural checking. It's a subtle distinction, but it's the difference between a build that passes and one that screams at you.
📋 Practical Task
Refactoring the Video Player Compatibility Layer
You are working on a media app. You have a function called playMedia that requires an object with a url and a duration. However, your app handles different types of media (Videos and Audio clips) which have different properties.
Your Task:
Implement the playMedia function and the corresponding types so that it can accept both a VideoFile and an AudioFile without using a union type (like VideoFile | AudioFile). Instead, rely on structural typing to ensure any object with the minimum required properties is accepted.
// 1. Define a type or interface called 'MediaElement'
// that contains the minimum requirements for playMedia.
// 2. Create a 'VideoFile' type that includes 'url', 'duration', and 'resolution'.
// 3. Create an 'AudioFile' type that includes 'url', 'duration', and 'bitrate'.
function playMedia(media: /* Your type here */) {
console.log(`Playing ${media.url} for ${media.duration} seconds.`);
}
const myVideo = {
url: "https://assets.com/intro.mp4",
duration: 120,
resolution: "1080p"
};
const myAudio = {
url: "https://assets.com/podcast.mp3",
duration: 3600,
bitrate: "320kbps"
};
// Test your implementation
playMedia(myVideo);
playMedia(myAudio);
There are no comments for now.