-
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
132: Common TypeScript Interview Questions on Structural Typing
If you've ever come from a language like Java or C#, TypeScript's type system can feel like it's gaslighting you. You define two completely different interfaces, you're certain they are distinct entities, and then TypeScript tells you they are exactly the same thing. This is the core of structural typing, and it's a favorite topic for interviewers because it reveals whether you actually understand how TypeScript works under the hood or if you're just guessing.
Let's look at a snippet I saw in a code review recently. The developer was trying to ensure that a function only processed "Clean" configuration objects, but they were running into a weird inconsistency with how errors were being reported.
interface AppConfig {
apiUrl: string;
timeout: number;
}
function initializeApp(config: AppConfig) {
console.log(`Connecting to ${config.apiUrl}...`);
}
// ❌ Error: Object literal may only specify known properties.
// 'retryAttempts' does not exist in type 'AppConfig'.
initializeApp({
apiUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3
});
const myConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3
};
// ✅ This works! No error.
initializeApp(myConfig);
The Excess Property Check Paradox
The developer was baffled. Why does the first call fail while the second one—passing the exact same data—succeeds? This is where most candidates trip up in interviews. They'll tell you the second one "should" be an error too.
Here is the deal: TypeScript is structurally typed. If an object has the required properties (the "shape"), it's compatible. myConfig has an apiUrl and a timeout. The fact that it also has retryAttempts doesn't matter; it satisfies the contract of AppConfig.
So why the error on the first call? That's a specific safety feature called Excess Property Checking. When you pass an object literal directly into a function, TypeScript assumes you're trying to be precise. If you add a property that isn't in the interface, you probably made a typo (e.g., writing apiURL instead of apiUrl). But once you assign that object to a variable, you're no longer passing a literal—you're passing a reference to an object that "fits the shape."
Shape Over Identity
In a nominal type system (like Java), if you have two classes with the exact same fields but different names, they are not interchangeable. In TypeScript, they are. I always tell my juniors to think of it as "Duck Typing" but with a compiler.
interface User {
id: string;
name: string;
}
interface Product {
id: string;
name: string;
}
const user: User = { id: "u1", name: "Alice" };
const product: Product = { id: "p1", name: "Laptop" };
// This is perfectly legal in TypeScript
const swap = (item: User) => console.log(item.name);
swap(product); // No error!
In an interview, you might be asked: "How do we prevent this?" Because in a real app, passing a Product into a User function is a recipe for a production outage. You can't just "make the interface different" because if the shapes are the same, TS sees them as the same.
Forcing Uniqueness with Branded Types
To get around structural typing when you actually need nominal typing, we use a technique called "Branding" or "Tagging." We add a property to the type that doesn't actually exist at runtime but exists for the compiler. It's essentially a lie we tell the type system to keep our data safe.
interface User {
__brand: "User"; // The brand
id: string;
name: string;
}
interface Product {
__brand: "Product"; // A different brand
id: string;
name: string;
}
const user = { id: "u1", name: "Alice" } as User;
const product = { id: "p1", name: "Laptop" } as Product;
const processUser = (u: User) => console.log(u.name);
processUser(user); // ✅ Works
processUser(product); // ❌ Error: Property '__brand' is incompatible.
By adding __brand, the shapes are no longer identical. The Product shape doesn't have a __brand of "User", so the structural check fails. I use this constantly for IDs (e.g., distinguishing a UserId from an OrderId when both are just strings) to prevent passing the wrong ID into a database query.
📋 Practical Task
Exercise: Implementing Branded IDs for a Banking System
You are building a banking application. Currently, AccountId and TransactionId are both defined as string. This is dangerous because a developer could accidentally pass a Transaction ID into a function that expects an Account ID, leading to catastrophic data errors.
Your Task:
- Create two "branded" types:
AccountIdandTransactionId. Both should be based on astringbut have unique brand properties to make them structurally incompatible. - Create a function
getAccountBalance(id: AccountId): numberthat returns a dummy balance. - Create a function
getTransactionDetails(id: TransactionId): stringthat returns a dummy detail string. - Implement a "type guard" or a casting mechanism (using
as) to create a validAccountIdandTransactionIdfrom raw strings. - Demonstrate that passing a
TransactionIdintogetAccountBalancenow triggers a TypeScript compiler error.
There are no comments for now.