skip to content

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%

answer

  1. not every alias is extendable
  2. the parent must have a member list
  3. unions have no single member list
  4. statically known members
  5. alias plus & is the escape hatch

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.

solid answer

~50 s

`extends` on an interface accepts anything that is an object type with statically known members: another interface, a class, an object-type alias such as `type Point = { x: number; y: number }`, an intersection of those, and a mapped type whose keys are fixed. It rejects an alias for a union, a primitive, or a bare type parameter — the error is "An interface can only extend an object type or intersection of object types with statically known members." The reason is structural: the compiler must be able to enumerate the inherited member list at the declaration in order to check the derived interface against it, and a union has no single member list. When you genuinely need to add members to a union, use an alias with `&` instead — `type Tagged = Shape & { id: string }` is legal and distributes over the union at the use site. You can also list several parents: `interface P extends Point, Named {}`.

code

typescript · 12 lines
typescript
type Point = { x: number; y: number };
type Named = { name: string };

// Object-type aliases, and several parents at once: accepted.
interface Point3D extends Point { z: number }
interface NamedPoint extends Point, Named { id: string }

// A union cannot be extended by an interface — use an alias with & instead.
type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
type Identified = Shape & { id: string };

const s: Identified = { kind: "square", side: 2, id: "a" };

go deeper

for a junior

Remember that extends accepts an object-type alias, an interface or a class, and that a union alias is refused — reach for an alias with & in that case.

for a middle

Explain the underlying requirement: the compiler must enumerate the parent's members at the declaration to build the derived member list and check assignability, which a union cannot supply.

for a senior

Know where the restriction bites in real APIs — augmenting a published discriminated union, or a generic mapped type — and reach for the intersection form deliberately rather than by trial and error.

for a principal

Frame it for a shared type library: prefer extendable object shapes in public surfaces so consumers can extend them, and be explicit that a union in the public API forecloses that path.

## What the clause actually requires An interface's `extends` clause is not restricted to other interfaces. The requirement is on the *kind of type*, not the syntax that produced it: the parent must be an **object type, or an intersection of object types, with statically known members**. That phrasing is lifted straight from the compiler's own diagnostic, and every accepted and rejected case follows from it. Accepted: ```typescript interface Named { name: string } type Point = { x: number; y: number }; type Timestamps = { created: Date } & { updated: Date }; type Fixed = { [K in "a" | "b"]: number }; class Widget { render(): string { return ""; } } interface P1 extends Named { id: string } // an interface interface P2 extends Point { z: number } // an object-type alias interface P3 extends Timestamps { id: string } // an intersection of object types interface P4 extends Fixed { c: number } // a mapped type with fixed keys interface P5 extends Widget { label: string } // a class's instance type interface P6 extends Named, Point { id: string } // several parents at once ``` Rejected: ```typescript type Shape = { kind: "circle" } | { kind: "square" }; type Id = string; // interface Tagged extends Shape { id: string } // Error: An interface can only extend an object type or // intersection of object types with statically known members. // interface Wrapper extends Id { } // same error: a primitive is not an object type ``` ## Why unions are refused Declaring `interface B extends A` obliges the compiler to do two things at the declaration site: build B's member list by adding B's own members to A's, and check that B is assignable to A. A union has no single member list — `Shape` above is *either* a circle or a square, and the properties available depend on which. There is no well-defined "inherit its members" operation, so the clause is refused rather than given a surprising meaning. "Statically known members" also excludes a mapped type over an unresolved type parameter, because until the parameter is instantiated the key set is unknown: ```typescript type Copy<T> = { [K in keyof T]: T[K] }; // interface Bad<T> extends Copy<T> { } // members are not statically known ``` A mapped type over a fixed key union, like `Fixed` above, is fine — its keys are decided the moment the alias is written. ## The intersection escape hatch This is the clearest case where the two composition forms are genuinely *not* interchangeable, and it is why the question gets asked. `&` is an operator, not a check, so it has no member-list requirement: ```typescript type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number }; type Identified = Shape & { id: string }; const c: Identified = { kind: "circle", r: 2, id: "a" }; // OK const s: Identified = { kind: "square", side: 2, id: "b" }; // OK ``` `Identified` still narrows on `kind` the way the original union did, with `id` available on both branches. If you had reached for an interface here you would have been stuck. ## Extending a class `interface I extends SomeClass` refers to the class's **instance type** — its properties and method signatures, with no implementations, and without the constructor or any static members. This is legal and occasionally useful, but note it also drags in the class's `private` and `protected` members, which restricts who can implement the interface. ## What this is not about None of this concerns which of `interface` or `type` you should reach for stylistically, and none of it survives to runtime: whichever form you pick, the emitted JavaScript contains no trace of it. The only thing at stake is whether the compiler will accept the declaration and what member list it computes. ## Quick checklist - Object-type alias, intersection of object types, interface, class instance type, fixed-key mapped type: `extends` works. - Union alias, primitive alias, unresolved generic mapped type: `extends` is refused; use `&` in an alias instead. - Multiple parents are allowed, but a property inherited from two of them must be identical in both unless you redeclare it.

  • You need to add an `id` property to an existing discriminated union. How do you do it?
    Use an alias and intersect: `type Identified = Shape & { id: string }`. The intersection distributes over the union at the use site, so each branch still narrows on its discriminant and gains `id`. An interface cannot do this — it has no way to inherit a union's members, which is precisely the case where alias and interface stop being interchangeable.
  • Why is a mapped type over a generic parameter rejected as a parent, when a mapped type over a fixed key union is fine?
    The clause needs the inherited member list at the declaration in order to build the derived list and check assignability. With fixed keys the members are decided when the alias is written. With `keyof T` they depend on an instantiation that has not happened yet, so the members are not statically known.

saying these in an interview costs you the question

  • Says extends only ever accepts another interface
  • Thinks an interface can extend a union of object types
  • Claims a primitive alias can be extended if it is boxed
  • Believes extending a class also brings in its static members
  • Assumes the alias must be declared before the interface in the file

context