Structural Typing & Duck Typing

10 questions found

What does 'structural typing' mean, and how does it differ from the 'nominal typing' used in languages like Java or C#?

Beginner
Structural typing determines type compatibility based purely on the SHAPE of a type — what properties/methods it has — rather than its declared NAME or explicit inheritance relationship (nominal typing). If two unrelated types happen to have the same shape, TypeScript treats them as compatible, unlike Java/C# which require an explicit class/interface relationship.
interface Point { x: number; y: number; }
class Vector { constructor(public x: number, public y: number) {} }
function printPoint(p: Point) { console.log(p.x, p.y); }
printPoint(new Vector(1, 2)); // works! Vector has the same SHAPE as Point, despite no declared relationship
Real-world example Passing a plain object literal or an instance of an unrelated class anywhere an interface is expected, as long as the shape matches.

Common follow-ups: What's the common phrase used to describe this kind of shape-based compatibility, borrowed from Python?

Types & Interfaces

What is 'duck typing' and how does the phrase 'if it walks like a duck and quacks like a duck' apply to TypeScript?

Beginner
Duck typing (the phrase's origin) means a value's suitability is judged by whether it HAS the behavior/shape you need, not by what it's formally declared to be — TypeScript's structural type system directly embodies this: any value with the right shape is accepted, regardless of its declared type name or class hierarchy.
interface Quacks { quack(): void; }
function makeItQuack(thing: Quacks) { thing.quack(); }
makeItQuack({ quack: () => console.log('Quack!') }); // works -- has the right shape, no formal 'Duck' type needed
Real-world example Accepting any object with the right method/property shape as a valid argument, without requiring it to formally implement an interface.

Common follow-ups: Does this mean you never actually NEED to use 'implements' in TypeScript for structural compatibility to work?

Types & Interfaces

What is 'excess property checking', and why does it seem to contradict pure structural typing at first glance?

Intermediate
When assigning an OBJECT LITERAL directly (not through a variable) to a typed variable/parameter, TypeScript performs a STRICTER check that flags any extra properties not in the target type — this seems to violate pure structural typing ('having more properties should be fine'), but it's a deliberate, pragmatic safety net specifically for catching typos in literals, since a literal has no other legitimate purpose than matching that exact shape.
interface Point { x: number; y: number; }
function printPoint(p: Point) { console.log(p.x, p.y); }
printPoint({ x: 1, y: 2, z: 3 }); // Error: object literal has excess property 'z'

const obj = { x: 1, y: 2, z: 3 };
printPoint(obj); // OK! Assigned via a variable, so excess property checking doesn't apply
Real-world example Catching a typo like 'colour' instead of 'color' in a configuration object literal that would otherwise silently pass structural checks.

Common follow-ups: Why does excess property checking specifically NOT apply when the object comes from a variable instead of a literal?

Never Unknown & Void Types

How does structural typing handle two interfaces that are 'shape-identical' but declared completely independently?

Intermediate
Since compatibility is purely shape-based, two independently-declared interfaces (or a class and an interface, or two unrelated classes) with IDENTICAL public shapes are considered fully interchangeable everywhere — TypeScript doesn't care that they have different names or were never explicitly related.
interface Named { name: string; }
interface Labeled { name: string; }
function greet(entity: Named) { console.log(entity.name); }
const item: Labeled = { name: 'Widget' };
greet(item); // works fine, despite 'Labeled' having no declared relationship to 'Named'
Real-world example Passing values between two independently-designed modules or libraries that happen to use identically-shaped but differently-named types.

Common follow-ups: Does this same interchangeability apply if the two types have PRIVATE members with the same names, as covered in the Classes topic?

Classes: Access Modifiers Abstract Classes & Implements

How does structural typing handle a function type that has FEWER required parameters than expected, versus one with MORE?

Intermediate
A function with FEWER declared parameters is structurally compatible where MORE are expected (since it simply ignores extra arguments passed to it, which is safe), but a function requiring MORE parameters than the target type provides is NOT compatible, since it would be called without enough arguments — matching how real function calls behave at runtime.
type Callback = (a: number, b: number) => void;
const shortFn: Callback = (a) => console.log(a); // OK -- fewer params is fine
// const longFn: Callback = (a, b, c) => console.log(a, b, c); // Error: too many required params
Real-world example Passing a simplified callback like `(item) => ...` where an array method's callback type technically expects `(item, index, array) => ...`.

Common follow-ups: Why is this particular asymmetry considered safe from a pure function-call perspective?

Function Types Overloads & Optional/Default Parameters

How would you deliberately opt OUT of structural typing for a specific type using the 'branded type' (nominal typing emulation) technique?

Advanced
Add a unique, otherwise-unused 'brand' property (often typed with a unique symbol or a literal string tag) to a type — this forces TypeScript's structural check to require that exact brand property, which only values EXPLICITLY constructed as that branded type can satisfy, effectively simulating nominal typing on top of a structural type system.
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };

function createUserId(id: string): UserId { return id as UserId; }
function getUser(id: UserId) { /* ... */ }

const pid = 'p123' as ProductId;
// getUser(pid); // Error: ProductId is not assignable to UserId, despite both being strings
Real-world example Preventing an entire class of bugs where two conceptually different IDs (both plain strings underneath) get accidentally swapped as function arguments.

Common follow-ups: What's the runtime cost (if any) of using branded types, given the brand property doesn't actually exist on real string values?

Types & Interfaces

Why does TypeScript's structural typing allow assigning a class instance to an interface even when the class has EXTRA methods beyond what the interface requires?

Advanced
Structural typing (outside the specific excess-property-check-on-literals case) is fundamentally about the target type's requirements being a SUBSET of what the source provides — having additional, unused capabilities is always safe from the consumer's perspective, since nothing requires those extra members to be used or even acknowledged.
interface Greetable { greet(): void; }
class Person {
  greet() { console.log('Hi'); }
  walk() { console.log('Walking'); } // extra method, not required by Greetable
}
const g: Greetable = new Person(); // OK -- Person satisfies AND exceeds Greetable's shape
Real-world example Passing a fully-featured class instance to a function that only cares about (and only interacts with) a small subset of its capabilities.

Common follow-ups: How does this relate to the Interface Segregation Principle from object-oriented design?

Classes: Access Modifiers Abstract Classes & Implements

How does structural typing's handling of optional properties create potential unsoundness when assigning a type with FEWER properties to one expecting an optional property?

Advanced
Because an optional property `prop?: T` is really `prop?: T | undefined` from the type checker's perspective, a type that OMITS that property entirely (never declares it at all) is structurally compatible with one that has it as optional — this is generally safe, but combined with excess property checking being SKIPPED for non-literal assignments, it can occasionally let genuinely mismatched shapes through variables rather than literals.
interface Options { timeout?: number; }
interface Empty {}
const e: Empty = {};
const opts: Options = e; // OK -- Empty structurally satisfies Options, since 'timeout' is optional
Real-world example Understanding why a completely unrelated, minimal object type can still satisfy an interface consisting entirely of optional properties.

Common follow-ups: Would this same assignment succeed if 'timeout' were a REQUIRED property instead of optional?

Nullability: strictNullChecks Optional Chaining & Nullish Coalescing

How do generic type parameters complicate structural comparisons, particularly regarding 'bivariant' method comparison?

Advanced
By default, TypeScript compares METHOD parameters BIVARIANTLY (allowing assignment in either direction, for practical/legacy reasons) but compares STANDALONE function type parameters (arrow function properties) more strictly, CONTRAVARIANTLY — meaning identically-shaped generic interfaces can behave differently for type-safety purposes depending on whether a callback is declared as a method shorthand or an arrow-function-typed property.
interface Comparer<T> {
  compare(a: T, b: T): number;      // method syntax: bivariant, more lenient
  compareArrow: (a: T, b: T) => number; // arrow property: contravariant, stricter with strictFunctionTypes
}
Real-world example Debugging a subtle type-safety difference between two seemingly-identical callback interface members, one declared as a method and one as an arrow-typed property.

Common follow-ups: How does the 'strictFunctionTypes' compiler option specifically affect this bivariance for standalone (non-method) function types?

Generics

How would you leverage structural typing to design a flexible 'partial dependency injection' interface for testing, accepting any object with the needed shape rather than requiring a specific concrete class?

Advanced
Define a narrow interface describing ONLY the specific methods/properties your code actually depends on (rather than depending on a full concrete class or library type) — thanks to structural typing, ANY object satisfying that minimal shape (a real service, an in-memory fake, or a test double) can be passed in without needing formal inheritance or 'implements' declarations.
interface Logger { log(message: string): void; }
class ProductionLogger implements Logger { log(m: string) { /* sends to real service */ } }
class FakeLogger implements Logger { logs: string[] = []; log(m: string) { this.logs.push(m); } }

function doWork(logger: Logger) { logger.log('working...'); }
doWork(new FakeLogger()); // trivially swappable in tests, thanks to structural typing
Real-world example Designing highly testable code by depending on minimal, narrowly-scoped interfaces rather than concrete implementation classes.

Common follow-ups: How does this structural-typing-driven design relate to the Dependency Inversion Principle from SOLID?

Classes: Access Modifiers Abstract Classes & Implements