Structural Typing, Interfaces and Assignability
TypeScript compares types by shape, not by name. Know when shape matching is relaxed (extra properties, optional members, method parameters) and when it is tightened (fresh literals, private members, readonly arrays).
Key points
- 1
A value is assignable when it has at least the target's required members with compatible types; extra members are fine and class names do not matter.
- 2
Excess property checks apply only to fresh object literals checked directly against a type: assignments, annotated returns, array elements and nested literals. Variables, assertions and spreads bypass them.
- 3
Interfaces merge across declarations and give clearer errors; type aliases can name unions, tuples and computed types but cannot be reopened. Only object type aliases get an implicit index signature.
- 4
readonly T[]is not assignable toT[], but property-levelreadonlyis ignored by assignability, so the same object can be mutated through another alias. - 5
Under strictFunctionTypes, function-typed properties check parameters contravariantly, while method syntax stays bivariant; arrays are covariant, which is unsound for writes.
- 6
private,protectedand#privatemembers make class compatibility nominal for those members; branded types (string & { __brand: "X" }) fake nominal typing for primitives. - 7
{}accepts everything except null and undefined;unknownaccepts everything but is assignable only tounknownandany;anyis assignable to everything exceptnever.
Common traps
An arrow function assigned to
() => Cfgdoes not get excess property checks on its returned literal; annotate(): Cfg =>instead.An interface is not assignable to
Record<string, string>even when an identical type alias is.Widening to a smaller type and then to a type with an optional property of a different type compiles, and can crash at runtime.
Read the source
Test yourself on Structural Typing, Interfaces and Assignability
Ten questions, with the answer and explanation after each one.