Suggest an editImprove this articleRefine the answer for “Structural typing (duck typing) in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Structural typing** in TypeScript means type compatibility is based on shape, not name. If an object has the required properties with matching types, it fits, no `implements` needed. ```typescript interface HasX { x: number; } const obj = { x: 10, extra: "ignored" }; // no implements function useX(item: HasX) { return item.x * 2; } useX(obj); // 20 ✅ ``` **Key:** shape match is enough. Use branded types when two structurally identical types must stay semantically separate.Shown above the full answer for quick recall.Answer (EN)Image**Structural typing** in TypeScript checks object shape (properties and their types), not the class name or declaration. If an object has everything a type requires, it passes. ## Theory ### TL;DR - Shape match beats name match. An object with `x: number` satisfies `interface HasX { x: number }` without any `implements` - Extra properties on an object passed via variable? Fine. Extra properties on a fresh object literal? Error - TypeScript checks structure at compile time only. Zero runtime cost - Java/C# check names; TypeScript checks structure. That is the whole difference - When two identical shapes represent different things (currencies, IDs), use branded types to enforce separation ### Quick Example ```typescript interface HasX { x: number; } const point = { x: 10, extra: "ignored" }; // no "implements HasX" function useX(item: HasX): number { return item.x * 2; } console.log(useX(point)); // 20 ✅ shape matches ``` `point` has an extra property, but TypeScript only checks that `x: number` exists. It does, so the call compiles. The extra property is ignored. ### Key Difference: Shape vs. Name In Java or C#, two classes with identical methods are still distinct types unless one declares `implements` or `extends` the other. TypeScript does not look at names. If the required properties exist with matching types, the types are compatible. ```typescript interface Cat { meow(): void; } interface CatLike { meow(): void; } const cat: Cat = { meow() {} }; const catLike: CatLike = cat; // ✅ same structure, compatible ``` In a nominal system this would fail. `Cat` and `CatLike` are separate names, so they are separate types regardless of content. ### When to Use - Third-party integrations: accept any object with the required shape, no wrapper classes needed - React props: any component that passes the right shape works, no explicit class inheritance - Express middleware: custom `req` extensions are accepted because the shape grows structurally - Avoid when shapes must stay semantically distinct. Currencies, user IDs, entity keys: use branded types instead ### Structural vs. Nominal | Aspect | Structural (TypeScript) | Nominal (Java/C#) | |---|---|---| | Compatibility basis | Shape match | Name + explicit declaration | | Extra properties | Allowed via variable | Not applicable | | `implements` keyword | Optional | Required | | Error style | Shape drift can go unnoticed | Explicit declaration mismatch | | Best for | Evolving APIs, libraries | Strict contracts, financial systems | ### Excess Property Checking Structural typing has one special rule. Object literals assigned directly to a typed variable trigger **excess property checking**. Pass the same object through a variable first, and the check disappears. ```typescript interface Config { host: string; port: number; } // ❌ direct literal - excess property error const cfg: Config = { host: "localhost", port: 3000, debug: true }; // Error: 'debug' does not exist in type 'Config' // ✅ through a variable - no excess check const obj = { host: "localhost", port: 3000, debug: true }; const cfg2: Config = obj; // Works ``` This asymmetry trips people up. Both cases are structural, but the first gets extra attention because TypeScript sees a fresh object it knows everything about. ### How the Compiler Handles This TypeScript's compiler (`tsc`) runs structural checks during type assignment: every required member of the target type must appear in the source with a compatible type. No runtime cost. Types are erased before the JavaScript runs, so Node.js and the browser never see them. Class instances follow the same rule. Public members only: ```typescript class Animal { name: string; constructor(n: string) { this.name = n; } } class Person { name: string; constructor(n: string) { this.name = n; } } let a: Animal = new Person("Alice"); // ✅ same public shape ``` Add `private` members and the story changes. Each class owns its private fields, and TypeScript treats them as distinct per class, so two classes with private fields are no longer structurally compatible even if they look identical. ### Common Mistakes **Mistake 1: Treating shape drift as harmless** ```typescript interface Point { x: number; y: number; } const p = { x: 1, y: 2, z: 3 }; const p2: Point = p; // ✅ compiles function move(pt: Point) { // pt.z is accessible at runtime but TypeScript has no type info for it console.log((pt as any).z); // type safety is lost here } ``` The assignment compiles, but you lose access to `.z` through the type. Fix with an index signature or extend the interface explicitly. **Mistake 2: Assigning a narrower type to a wider one** ```typescript type A = { a: number }; type B = { a: number; b: string }; const narrow: A = { a: 1 }; const wide: B = narrow; // ❌ Missing property 'b' ``` `A` does not have `b`, so it cannot satisfy `B`. The reverse always works: a `B` can go anywhere an `A` is expected. **Mistake 3: Private members blocking structural compatibility** ```typescript class Dog { private breed: string; constructor(b: string) { this.breed = b; } bark() {} } class Cat { private breed: string; constructor(b: string) { this.breed = b; } bark() {} } const d: Dog = new Cat("Persian"); // ❌ private field mismatch ``` The `private breed` fields belong to different classes. TypeScript blocks this even though the classes look identical. **Mistake 4: Relying on structural typing for semantic safety** ```typescript type USD = number; type EUR = number; function convertToEUR(amount: USD): EUR { return amount * 0.85; } const euros: EUR = 100; convertToEUR(euros); // ✅ no error, but logically wrong ``` I've seen this exact pattern in financial code where two number aliases silently accepted each other for months before someone noticed. Both types are just `number`, so TypeScript cannot tell them apart. Branded types fix this. ### Real-World Usage - React: props interfaces accept any object with the correct shape, no explicit class needed - Express: middleware receives `(req: Request, res: Response)`; custom `req` properties work if you extend the type structurally - Redux: action creators return `{ type: string; payload?: any }`; any matching object can be dispatched - Node.js: stream-like objects plug into APIs without formal `implements` ### Follow-Up Questions **Q:** What is excess property checking and when does it trigger? **A:** It triggers when you assign an object literal directly to a typed variable or pass it as a direct argument. Passing through an intermediate variable skips the check because TypeScript treats the object as pre-existing. **Q:** Can two classes be structurally incompatible even with the same methods? **A:** Yes. Private or protected members make classes incompatible. Each class owns its private members, and TypeScript treats them as distinct even if the field names match. **Q:** How do you enforce nominal typing in TypeScript? **A:** Brand the type with a unique symbol: `type USD = number & { readonly __brand: unique symbol }`. This adds a phantom property that exists only at the type level and blocks accidental assignments. **Q:** Why does assigning a fresh object literal to a supertype fail? **A:** TypeScript applies excess property checking to fresh literals. If the target type does not declare the extra property, TypeScript errors. Assign to a variable first and the check is skipped. **Q:** Does structural typing have any runtime cost? **A:** None. All type information is erased before JavaScript runs. Structural checking is a compile-time operation only. **Q:** (Senior) A function type is `(e: string) => void`. Can you assign `(e: string, n: number) => void` to it? **A:** No. Function parameters check contravariantly. The extra parameter `n` makes the signatures incompatible because the caller does not provide it. ## Examples ### Basic: Object Shape Matching ```typescript interface User { id: number; name: string; } // extra property 'role' - no implements, no class const admin = { id: 1, name: "Alice", role: "admin" }; function greet(user: User): string { return `Hello, ${user.name}`; } console.log(greet(admin)); // "Hello, Alice" ✅ ``` `admin` has an extra `role` property, but `User` only requires `id` and `name`. TypeScript sees both, so the call is valid. `role` is simply outside the type's view. ### Intermediate: React Component Props ```typescript interface ButtonProps { children: React.ReactNode; onClick?: () => void; className?: string; } const Button = ({ children, onClick, className }: ButtonProps) => ( <button onClick={onClick} className={className}> {children} </button> ); const handleClick = () => console.log("clicked"); const usage = ( <Button onClick={handleClick} className="btn-primary"> Submit </Button> ); // ✅ shape matches ButtonProps ``` `ButtonProps` does not care where `handleClick` comes from or how it was declared. It only checks the function signature shape. This is structural typing doing its job in real production code. ### Advanced: Branded Types for Nominal Behavior When two types share the same structure but must stay separate, branded types add a type-level marker: ```typescript // plain aliases - structurally identical, TypeScript cannot tell them apart type USD = number; type EUR = number; // branded aliases - structurally different at the type level type BrandedUSD = number & { readonly __brand: unique symbol }; type BrandedEUR = number & { readonly __brand: unique symbol }; // constructor functions are the only way to create values function makeUSD(n: number): BrandedUSD { return n as BrandedUSD; } function makeEUR(n: number): BrandedEUR { return n as BrandedEUR; } function convertToEUR(amount: BrandedUSD): BrandedEUR { return (amount * 0.85) as BrandedEUR; } const dollars = makeUSD(100); convertToEUR(dollars); // ✅ convertToEUR(makeEUR(100)); // ❌ BrandedEUR is not assignable to BrandedUSD ``` The `__brand` property never exists at runtime. It is a phantom field TypeScript uses only during type checking. This is the standard way to get nominal-like behavior from a structural type system.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.