Interfaces & Types
Interfaces as structural contracts, interface versus type alias, structural typing, and declaration merging.
13 questions
JuniorTheoryVery commonWhat is an interface in TypeScript?
What is an interface in TypeScript?
An interface describes the shape of an object — the names and types of its properties and methods — as a compile-time contract. It supports optional members (?), readonly members, method and call signatures, and index signatures. A class can implements an interface, and object literals are checked against it. Interfaces are erased at compile time, so they add no runtime cost.
Common mistakes
- ✗Thinking an interface exists at runtime — it is fully erased after compilation
- ✗Confusing an interface with a class — an interface has no method bodies or constructor
- ✗Forgetting that
?marks a member optional andreadonlyforbids reassignment
Follow-up questions
- →How does
implementsdiffer fromextendswhen used on a class? - →What does an index signature let you express in an interface?
MiddleTheoryVery commonWhen would you use an interface versus a type alias?
When would you use an interface versus a type alias?
Both can describe object shapes and are largely interchangeable there. The differences: interface supports declaration merging and is the convention for object and class contracts and public APIs; a type alias can also express unions, intersections, tuples, mapped and conditional types, and alias primitives — none of which interface can. Rule of thumb: reach for interface for extendable object and class shapes, and type for unions and computed types.
Common mistakes
- ✗Believing the two are perfect synonyms — only
typedoes unions and computed types - ✗Thinking
interfacecan express a union or alias a primitive — it cannot - ✗Assuming either keyword affects runtime — both are erased at compile time
Follow-up questions
- →Which one can a class
implements, and can it implement both? - →Why can't a
typealias be reopened the way an interface can?
JuniorTheoryCommonWhy does an unexpected extra property fail when you assign an object literal directly?
Why does an unexpected extra property fail when you assign an object literal directly?
Excess property checking is an extra rule for fresh object literals: one assigned or passed directly is rejected if it has a member the target type does not declare. Structural typing alone would accept it; the rule catches typos like widht.
Common mistakes
- ✗Believing structural typing forbids extra members in general
- ✗Expecting the check to apply to a value that arrived through a variable
- ✗Reading the error as being about the emitted JavaScript rather than about a typo
Follow-up questions
- →How does assigning the literal to a variable first change the result?
- →What does adding an index signature to the target type do to this check?
JuniorTheoryCommonWhy does an object literal you never declared as a Point satisfy a Point parameter?
Why does an object literal you never declared as a Point satisfy a Point parameter?
TypeScript compares types by shape, not by name. A value fits Point as soon as it has the members Point requires: no implements, no import, no declared relationship. In a nominal language like Java the name must match and the code is rejected.
Common mistakes
- ✗Thinking a class or object must declare
implementsto satisfy an interface - ✗Expecting two identically shaped types from different modules to be incompatible
- ✗Assuming assignability compares property names but not their types
Follow-up questions
- →What extra check does a fresh object literal get that a variable does not?
- →How would you make two identically shaped types deliberately incompatible?
MiddleTheoryCommonWhat is structural typing in TypeScript?
What is structural typing in TypeScript?
TypeScript uses structural ("duck") typing — compatibility is decided by an object's shape, not by its declared name. An object is assignable to a type if it has the required members, even if it never explicitly declares that interface, and two unrelated types with the same members are compatible. Fresh object literals additionally get an excess-property check. This contrasts with nominal typing in Java or C#, where the declared name must match.
Common mistakes
- ✗Assuming a value must declare the interface by name — only its shape matters
- ✗Expecting two same-shaped named types to be incompatible — they are compatible
- ✗Forgetting the excess-property check applies only to fresh object literals
Follow-up questions
- →Why does assigning an extra-property object literal sometimes error?
- →How would you simulate nominal typing when you actually need it?
MiddleTheoryOccasionalWhy does the same object with an extra property pass when assigned through a variable?
Why does the same object with an extra property pass when assigned through a variable?
The literal loses its freshness once bound to a variable, and only fresh literals get the excess-property check. Plain structural typing is left: it asks only whether the required members are there. The value is assignable, and the typo lives on.
Common mistakes
- ✗Thinking the extra property is stripped from the object at run time
- ✗Expecting the excess-property check to follow the value through a variable
- ✗Concluding the variable's type became
any
Follow-up questions
- →How would
satisfieson the variable restore the check? - →Why does TypeScript not simply make excess properties an error everywhere?
MiddleTheoryOccasionalWhat does adding an index signature force on the interface's other members?
What does adding an index signature force on the interface's other members?
Every declared property must fit the signature's value type, so { [k: string]: number; name: string } is an error. The key must be string, number, symbol or a template literal. A read is unchecked: obj.missing is number, not undefined.
Common mistakes
- ✗Declaring a property whose type does not fit the index signature's value type
- ✗Expecting a read through an unknown key to be typed
T | undefinedby default - ✗Assuming an index signature key may be any type
Follow-up questions
- →How does a union in the value type let a mixed-shape object still type-check?
- →What exactly does
noUncheckedIndexedAccesschange about the read?
MiddleTheoryOccasionalWhen would you rely on interface merging instead of simply writing extends?
When would you rely on interface merging instead of simply writing extends?
Use extends when you own the type: it is explicit and the new name says what it is. Merging is for the case you cannot rename — adding a member to a type you do not own, like Express.Request or Window. Existing references see the addition.
Common mistakes
- ✗Reaching for merging where a plain
extendsand a new name would be clearer - ✗Expecting a merged declaration to override a member with an incompatible type
- ✗Thinking existing references keep seeing the pre-merge shape
Follow-up questions
- →How do you merge into a type that lives inside an external module?
- →What happens when two declarations give the same member incompatible types?
MiddleTheoryOccasionalHow does Record<K, V> differ from an index signature { [k: string]: V }?
How does Record<K, V> differ from an index signature { [k: string]: V }?
Both describe an object whose values share a type V, but they constrain keys differently. An index signature { [k: string]: V } allows any key of that primitive kind. Record<K, V> is a mapped type over a known key set K, which may be a finite union of string literals like Record<'a' | 'b', V> — then exactly those keys must exist. So Record can enforce a fixed key set; an index signature stays open-ended.
Common mistakes
- ✗Thinking
Recordand an index signature are always interchangeable - ✗Believing
Record<'a' | 'b', V>leaves keys open like{ [k: string]: V } - ✗Assuming
Recordis a runtime container rather than a compile-time type
Follow-up questions
- →When would a finite-union
Recordcatch a bug an index signature would miss? - →Can an index signature and explicit named properties coexist on one type?
SeniorTheoryOccasionalWhat is declaration merging for interfaces in TypeScript?
What is declaration merging for interfaces in TypeScript?
Declaring the same interface name more than once merges the declarations into one interface whose members are combined. This is used to augment existing types — for example adding properties to Window, or extending a library's types via module augmentation. Only interfaces and namespaces merge this way; a duplicate type alias of the same name is an error. Members with the same name but incompatible types across the declarations are also an error.
Common mistakes
- ✗Expecting a redeclared interface name to be a duplicate-identifier error
- ✗Thinking a duplicate
typealias merges the same way an interface does - ✗Assuming the last declaration replaces the earlier members instead of combining them
Follow-up questions
- →How does module augmentation add a property to a third-party type?
- →What happens if two merged members declare the same name with different types?
SeniorTheoryOccasionalWhy can a type alias not be reopened the way an interface can?
Why can a type alias not be reopened the way an interface can?
An interface is an open name for a shape, so a second declaration can add members. An alias is a closed binding to a computed type — a union, a conditional — with no shape to add to. A second alias of the same name is a duplicate identifier.
Common mistakes
- ✗Expecting two aliases of the same name to intersect the way interfaces merge
- ✗Explaining the restriction by runtime emit rather than by the alias being a closed binding
- ✗Assuming an alias can be augmented from a consumer's module
Follow-up questions
- →What does this mean for a library that wants its types to be extensible by consumers?
- →How would you emulate an extensible alias without giving up unions?
SeniorTheoryRareTypeScript has no exact object type — what does excess-property checking give you instead?
TypeScript has no exact object type — what does excess-property checking give you instead?
A shallow, freshness-scoped approximation: a literal written at the assignment site is checked, nothing else is. A value arriving via a variable, a return or a spread carries its extras through, since structural typing asks only for required members.
Common mistakes
- ✗Reading excess-property checking as a genuine exact-type guarantee
- ✗Expecting the check to follow a value through a variable, a return, or a spread
- ✗Believing extra members are stripped from the emitted object
Follow-up questions
- →How would you approximate an exact type with a mapped type and
never? - →Why would a truly exact object type break structural assignability across a codebase?
SeniorTheoryRareHow does a namespace merge with a class or function of the same name?
How does a namespace merge with a class or function of the same name?
The namespace's exported members become static members of the value: function f() {} plus namespace f { export const v = 1 } gives f.v. The two occupy different declaration spaces, so the compiler attaches rather than clashes.
Common mistakes
- ✗Expecting the two declarations to collide as a duplicate identifier
- ✗Declaring the namespace before the function or class it should attach to
- ✗Believing the members land on the prototype rather than on the value itself
Follow-up questions
- →Which declarations can merge with which — and which pairs are an error?
- →How would you express the same shape with ES modules instead?