skip to content

Interfaces & Type Aliases

The constructs you use to describe the shape of an object in TypeScript — interfaces, type aliases, and the structural rules that decide when one shape is assignable to another. Interviewers open almost every TypeScript screen here because 'interface or type?' quickly exposes whether you understand declaration merging, structural typing, and excess property checks or just memorized syntax.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

TypeScript checks type compatibility structurally rather than nominally. What does that mean for a function whose parameter is typed `interface Point { x: number; y: number }` — which values will the compiler accept?

level: juniorimportance: must knowfreq 78%

answer

  1. names don't decide compatibility
  2. compare member by member
  3. source may have more, never less
  4. implements is optional, not enabling

basics

~20 s

TypeScript compares types by shape, not by declared name. Any value whose type has x and y of type number is accepted for a Point parameter, no matter how it was declared, and extra members do not disqualify it.

solid answer

~50 s

Structural typing means the compiler asks "does this value have the members `Point` requires, with compatible types?" rather than "was this value declared as a `Point`?". So an object literal stored in a variable, an instance of a class that never writes `implements Point`, or a value of a completely unrelated interface declared in another file all pass, as long as they carry `x: number` and `y: number`. The check runs member by member: every required member of the target must exist in the source with an assignable type. Members the target does not mention are simply ignored, so a richer value is accepted where a narrower type is expected. Nominal languages like Java or C# would reject all of these without an explicit declaration; TypeScript accepts them because the shape is what it models. This is often called duck typing: if it has the members, it counts.

code

typescript · 12 lines
typescript
interface Point { x: number; y: number }

function length(p: Point): number {
  return Math.hypot(p.x, p.y);
}

class Vec { x = 3; y = 4; z = 0 }

const reading = { x: 3, y: 4, sensor: "a1" };

console.log(length(new Vec()));
console.log(length(reading));

go deeper

for a junior

Be ready to say plainly that TypeScript compares the shape of a value, not the name it was declared with, and that an object carrying the required members is accepted with no implements clause anywhere.

for a middle

Explain the member-by-member mechanics: every member the target requires must exist on the source with an assignable type, the comparison recurses into nested objects, and members the target never mentions are ignored.

for a senior

Show where shape-based compatibility misleads a team — two unrelated domain types with identical members swapping silently — and say how you catch that in review and at the boundaries where data enters the system.

for a principal

Own the tradeoff: structural compatibility is why third-party and generated types compose without adapters, but it also means the published contract of your module is a shape, not a name, and any identity you need must be designed in deliberately.

## The rule TypeScript's assignability relation is **structural**: a source type `S` is assignable to a target type `T` when `S` has, member for member, everything `T` requires, with types that are themselves assignable. Nothing about *where* the type was declared, what it is *called*, or whether an `implements` clause was written enters into the decision. Contrast this with a **nominal** system (Java, C#, Kotlin). There, `class Vec` is a `Point` only if the author of `Vec` said so. In TypeScript, saying so is optional — it is a way of asking the compiler to check your class early, not the mechanism that makes the value acceptable. ```ts interface Point { x: number; y: number } function length(p: Point) { return Math.hypot(p.x, p.y); } class Vec { x = 3; y = 4; z = 0 } const reading = { x: 3, y: 4, sensor: "a1" }; length(new Vec()); // ok — no implements clause anywhere length(reading); // ok — sensor is simply not looked at ``` ## How the check actually runs For each member the target declares, the compiler looks the same name up on the source: 1. **Missing required member → error.** `{ x: 1 }` is not a `Point`; `y` is absent. 2. **Present but wrong type → error.** `{ x: 1, y: "4" }` fails on `y`, and the failure is reported as a nested reason ("types of property 'y' are incompatible"). 3. **Present and assignable → keep going.** The member check is itself recursive: nested objects are compared the same way, all the way down. 4. **Extra members on the source → ignored.** The target never says "and nothing else", so a wider value satisfies a narrower type. This is *width subtyping*. Because the relation is defined on shapes and not on names, two interfaces written independently in two files, with no import between them, are interchangeable if their members line up. That is the single most surprising consequence for someone arriving from a nominal language, and it is what the term *duck typing* points at: if it walks and quacks like a `Point`, it is one. ## Two clarifications interviewers listen for **Interfaces are a compile-time device only.** An `interface` emits no JavaScript. There is no runtime object representing `Point`, no tag stamped on values, nothing to check with `instanceof`. Structural typing is not the compiler being lax at runtime — there *is* no runtime type layer at all. A value that satisfied `Point` at the call site is just an ordinary object once compiled. **Structural does not mean "anything goes".** The compiler is strict about what the target requires; it is only silent about what the target does not mention. Excess members are allowed on values, but a *fresh object literal* written directly at the assignment site gets an additional check for unknown properties layered on top — a separate rule, and a common source of the false belief that TypeScript is nominal about object shapes. ## Why the language is designed this way JavaScript code is full of ad-hoc object shapes: options bags, JSON parsed from a response, the result of a `map`, an object spread. Almost none of it was authored against a named type. A nominal system would force an adapter or a cast at every boundary; structural typing lets you *describe* an existing shape after the fact and have existing values satisfy the description immediately. It is also what makes third-party `.d.ts` files usable — your object satisfies a library's interface without your code depending on the library at the value level. The cost is that shape, not name, is your contract. Two domain concepts that happen to be `{ value: number }` — metres and feet, gross and net — are freely interchangeable, and the compiler will never object. That is a real class of production bug, and the reason type systems that need identity add something extra on top of the structure. ## Answering it well Say the rule in one line ("compatibility is decided by members, not by names"), name the direction (a source may have *more* than the target requires, never less), and mention that `implements` is a checking convenience rather than the enabling mechanism. If you get one more sentence, add that the whole comparison disappears at compile time — the emitted JavaScript contains no trace of `Point`.

  • If a class does not need an `implements` clause to satisfy an interface, what does writing one actually buy you?
    It moves the error. Without it, a mismatch is reported at every call site that passes the instance; with it, the class declaration itself fails to compile and names the missing or wrong member. It also documents intent for readers. It never changes whether the value is accepted elsewhere — that is still decided structurally.
  • Does structural typing mean two identically-shaped domain types can be swapped by accident?
    Yes. If `Metres` and `Feet` are both `{ value: number }`, each is assignable to the other and the compiler stays silent, because nothing distinguishes them structurally. Making them distinguishable requires giving one a member the other does not have — the type system will not invent an identity for you.
  • How is a nested member compared — by name too, or by shape?
    By shape, recursively. If the target requires `origin: Point`, the compiler applies the same member-by-member test to whatever the source's `origin` is. The comparison bottoms out at primitives, and a failure deep in the tree is reported as a chain of "types of property … are incompatible" reasons.

A bouncer checking a dress code rather than a guest list: he looks at what you are wearing, not whether your name was written down in advance.

saying these in an interview costs you the question

  • Says a class must declare implements before it can be used
  • Claims interfaces exist at runtime and can be checked
  • Thinks two same-shaped interfaces are incompatible across files
  • Believes extra members always make a value incompatible
  • Calls structural typing "the compiler being loose" rather than a rule

context

open as a page

In TypeScript, what does the type `(id: number) => Promise<User>` describe, and must the implementing function name its parameter `id`?

level: juniorimportance: must knowfreq 72%

basics

~20 s

That type describes any function taking one number argument and returning a Promise of User. Parameter names inside a function type are labels for readers only — matching is positional, so an implementation may name the parameter anything.

open as a page

In TypeScript, which kinds of type can a `type` alias express that an `interface` declaration cannot, and how should that shape your choice between the two?

level: juniorimportance: must knowfreq 85%

basics

~20 s

A type alias can name any type — unions, tuples, primitives and types derived from other types — while an interface declares an object type only. Reach for an alias whenever the type is not a plain object shape.

open as a page

In TypeScript, a helper is supposed to receive the `Person` class itself, but `function make(ctor: Person)` rejects the argument `Person`. What does the type `Person` actually refer to, and how should the parameter be typed?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A class declaration creates two separate things: a type named Person that describes instances, and a value named Person that is the constructor. A parameter receiving the class itself must be typed typeof Person, or new (name: string) => Person.

open as a page

In TypeScript, what happens when the same interface name is declared twice in one scope, and what happens when the same `type` alias name is declared twice?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Two interface declarations sharing a name in the same scope merge into one type carrying both sets of members. Repeating a type alias name is a duplicate-identifier error instead: interfaces are open, aliases are closed.

open as a page

In TypeScript, how do you type an object whose property names are not known in advance — say a lookup from user id to user name — and what does that type then allow and forbid?

level: juniorimportance: must knowfreq 65%

basics

~10 s

Declare an index signature: interface UserNames { [id: string]: string }. Any string key may then be read or written and is typed string, but the type guarantees that no particular key actually exists.

open as a page

In a TypeScript interface, what does the `?` in `email?: string` change — for code that builds the object, for code that reads the property, and for the emitted JavaScript?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Marking a property with ? makes it optional: an object may omit the key, and reading it yields string | undefined under strictNullChecks. The modifier is compile-time only and changes nothing in the emitted JavaScript.

open as a page

In TypeScript, `const opts = { url: "/api", retries: 3 }` is passed to `function get(o: { url: string }) {}` as `get(opts)`. Does that compile, and what assignability rule decides it?

level: middleimportance: must knowfreq 64%

basics

~20 s

It compiles. Assignability requires the source to have at least the members the target declares; it never requires the source to have only those members, so the extra retries property is ignored. This allowance is called width subtyping.

open as a page

In TypeScript, `type UserId = string` and `type OrderId = string` are two separate aliases, yet a UserId value can be passed to a function expecting an OrderId. Why does that compile, and how does a branded type turn it into an error?

level: middleimportance: must knowfreq 55%

basics

~20 s

A type alias names a type, it does not create one: both aliases are exactly string, and TypeScript compares types by structure. Branding intersects one with a phantom property, string & { __brand: 'UserId' }, that a plain string does not have.

open as a page

In TypeScript, what does the type `new (name: string) => Person` describe, and which values are actually assignable to it?

level: middleimportance: must knowfreq 58%

basics

~20 s

It is a construct signature: it describes a value you invoke with new, taking a string and producing a Person. Classes and class expressions with a matching constructor are assignable; plain functions and arrow functions are not.

open as a page

Given `interface Point { x: number; y: number }` in TypeScript, why does `const p: Point = { x: 1, y: 2, z: 3 }` fail with "Object literal may only specify known properties", while `const tmp = { x: 1, y: 2, z: 3 }; const p: Point = tmp;` compiles cleanly?

level: middleimportance: must knowfreq 74%

basics

~20 s

TypeScript applies an extra excess-property check to fresh object literals assigned straight to a typed target. Storing the literal in a variable first widens it and drops that freshness, leaving only ordinary structural assignability, which happily permits extra properties.

open as a page

In TypeScript, both `interface B extends A { id: number }` and `type B = A & { id: number }` compose A's members with a new one. If A already declares `id: string`, how do the two forms behave differently?

level: middleimportance: must knowfreq 66%

basics

~20 s

extends overrides the inherited member and errors right at the declaration when the new type is not assignable to the old one. An intersection instead combines both types, so id becomes string & number, which reduces to never — a failure you only meet at the use site.

open as a page

In TypeScript, in what order does the compiler try the signatures in an overload list, and what goes wrong when a broader signature is written above a narrower one?

level: middleimportance: must knowfreq 55%

basics

~20 s

TypeScript reads an overload list top-down and commits to the first signature the arguments satisfy — not the most specific one. A broader signature listed above a narrower one therefore shadows it, and the narrower form's return type is never seen.

open as a page

In TypeScript, why does `interface Settings { [key: string]: string; retries: number }` fail to compile, and what are your options for fixing it?

level: middleimportance: must knowfreq 55%

basics

~20 s

The index signature promises that every string key holds a string, and retries is a string key holding a number, so TypeScript rejects it. Fix it by widening the signature's value type, moving the open bag into its own property, or dropping the signature.

open as a page

In a TypeScript object type, what is the difference between declaring `email?: string` and declaring `email: string | undefined`?

level: middleimportance: must knowfreq 66%

basics

~20 s

The optional form lets the key be omitted; the union form requires the key to be present, even if its value is undefined. Both read as string | undefined, and under default settings the optional form also accepts an explicit undefined.

open as a page

In TypeScript, reading a key that is absent from a dictionary type such as `{ [id: string]: User }` still type-checks and yields `User` rather than `User | undefined`. Why does the checker behave that way, and how do you get honest types for those lookups?

level: seniorimportance: must knowfreq 50%

basics

~20 s

By default an index-signature read is typed as the value type, because the checker cannot know which keys exist and assuming presence keeps everyday code ergonomic. Enable noUncheckedIndexedAccess to make such reads yield the value type plus undefined.

open as a page

In TypeScript, given `type UserId = string & { readonly __brand: 'UserId' }`, what JavaScript does `const id = 'u_1' as UserId` compile to, and what does reading `id.__brand` give you at runtime?

level: juniorimportance: should knowfreq 40%

basics

~20 s

It compiles to const id = 'u_1';. The type and the assertion are erased, so the value is an ordinary string primitive and id.__brand is undefined at runtime — the brand exists only during type checking.

open as a page

In TypeScript, an object carrying a property that its declared type does not list is passed to a function. Does the emitted JavaScript still contain that property at run time, and what does that mean if the function serialises the object?

level: juniorimportance: should knowfreq 44%

basics

~20 s

Yes, the property is still there. TypeScript erases types and never rewrites values, so nothing strips undeclared keys. The excess-property check is a compile-time typo guard only; serialising the object sends the extra key over the wire.

open as a page

In TypeScript, which kinds of `type` alias can an `interface` extend in its `extends` clause, and which ones does the compiler reject?

level: juniorimportance: should knowfreq 44%

basics

~20 s

An interface can extend an alias that resolves to an object type, or to an intersection of object types, as long as its members are statically known. Aliases for unions, primitives or unresolved type parameters are rejected.

open as a page

In TypeScript, a function is written as two overload signatures followed by one implementation signature. How many functions does the emitted JavaScript contain, and which of those signatures can a caller use?

level: juniorimportance: should knowfreq 50%

basics

~20 s

The emitted JavaScript contains exactly one function — overload signatures are types and are erased. Callers may use only the overload signatures; the implementation signature is invisible to them and cannot be called on its own terms.

open as a page

In TypeScript, which values are assignable to the type `{}`, how does that differ from the type `object`, and why is `{}` a poor way to say "some object"?

level: middleimportance: should knowfreq 40%

basics

~20 s

{} requires no members, so under strictNullChecks every value except null and undefined is assignable to it, primitives included. object excludes primitives but still accepts any non-primitive. Neither lets you read a property, so {} says almost nothing.

open as a page

In TypeScript, a value of type `{ id: string; name?: string }` is assigned to a variable of type `{ id: string; name: string }`. Does it compile, and does the reverse assignment compile?

level: middleimportance: should knowfreq 47%

basics

~20 s

The first fails: a member that is optional on the source may be absent, so it cannot satisfy a required member on the target. The reverse compiles, because a required member always satisfies an optional one.

open as a page

In TypeScript, how do you type a value that is both callable and carries properties — for example a `log(message)` function that also has `log.level` and `log.reset()` — and why can't the `(message: string) => void` shorthand express it?

level: middleimportance: should knowfreq 46%

basics

~20 s

Declare the callable as a call-signature member of an object type or interface, alongside the properties. The arrow shorthand is one closed type expression with nowhere to put extra members; an intersection of a function type and an object type is the equivalent.

open as a page

In TypeScript, you need a second value — a test double or a wrapper — to have exactly the same type as an existing function called `parseConfig`. How do you express that without retyping its signature by hand?

level: middleimportance: should knowfreq 44%

basics

~10 s

Use a typeof type query: writing typeof parseConfig in type position yields the type the compiler already inferred for that value, including its parameters, its return type, and any properties hung on the function.

open as a page

In TypeScript, a value typed by `interface Config { url: string }` is rejected when passed to a parameter of type `Record<string, unknown>`, while the same value typed by `type Config = { url: string }` is accepted. Why does the choice of declaration form change that?

level: middleimportance: should knowfreq 38%

basics

~20 s

Object-literal type aliases receive an implicit index signature; interfaces do not, because an interface can gain members from a later declaration, so the compiler will not treat its member list as final. Add an explicit index signature or use an alias.

open as a page

In TypeScript, why does passing an abstract class to a parameter typed `new () => Shape` fail to compile, and how do you type the parameter so it accepts one?

level: middleimportance: should knowfreq 38%

basics

~20 s

An abstract class cannot be instantiated, so its constructor type is an abstract constructor type, which TypeScript refuses to assign to a plain new () => Shape. Type the parameter abstract new () => Shape, which accepts abstract and concrete classes alike.

open as a page

In TypeScript, how do you add a typed property to the DOM `Window` interface from inside a module file, and what does the `declare global` block do there?

level: middleimportance: should knowfreq 55%

basics

~20 s

Wrap the declaration in a declare global block inside a module file: it reopens the global declaration space so interface Window { ... } merges with the DOM's Window instead of creating a new module-local interface.

open as a page

In TypeScript, how do you add a property to an interface that an npm package exports, and what rules must the `declare module 'pkg'` block obey?

level: middleimportance: should knowfreq 48%

basics

~20 s

Write a module augmentation: in a file that is itself a module, a declare module 'pkg' block re-declares one of the package's exported interfaces, and the members merge into the original. It can only patch existing declarations.

open as a page

Given `interface Options { retries?: number; timeout?: number }` in TypeScript, why does `const o = { retres: 3 }; const opts: Options = o;` still error, even though `o` is a variable rather than a fresh object literal?

level: middleimportance: should knowfreq 38%

basics

~20 s

Options is a weak type — every property is optional — so almost anything is structurally assignable to it. TypeScript adds a weak-type check: a source with properties but none in common with the target is rejected, and it applies to variables too.

open as a page

In TypeScript, the body of an overloaded function is type-checked against its implementation signature only. Given that, what does the compiler verify between the overload signatures and the implementation, and what does it not verify?

level: middleimportance: should knowfreq 45%

basics

~20 s

TypeScript checks only that each overload signature is compatible with the implementation signature, and that check is deliberately loose. It never verifies that the body returns the type a particular overload promised, so overloads are an unchecked promise.

open as a page

showing 1–30 of 51