Interfaces: Methods, Extension, Declaration Merging, and Choosing Interface vs Type

Learn to declare object and function-shaped contracts with interface, extend interfaces to build on existing shapes, understand declaration merging, and decide when an interface is the better tool over a type alias.

Interfaces are TypeScript's purpose-built tool for describing object shapes — including the methods an object exposes — and for building larger contracts out of smaller ones through extension.

Interface syntax, optional and readonly properties, and methods

An interface declares a named, structurally-checked contract for what an object must contain: interface User { id: number; name: string } means any object assigned to a User-typed location must have at least an id: number and a name: string — extra properties on the actual object are fine, because TypeScript checks structurally, not by name. As with object type literals, ? marks a property optional and readonly prevents reassignment through that typed reference, and both work identically inside interfaces.

Interfaces can declare methods in two equivalent-looking but slightly different styles: greet(): string; (a method signature) and greet: () => string; (a property whose type is a function). For most purposes these behave the same, but method-signature syntax is treated slightly more leniently under strictFunctionTypes for parameter variance, which is why library and DOM type definitions favor method syntax for anything meant to be widely implemented.

Because interface members are separated by semicolons or newlines rather than commas, a common slip for developers coming from object-literal syntax is to accidentally use commas between properties — this is a straightforward syntax error and one of the most common first mistakes when learning interfaces.

Function interfaces

Beyond describing objects with named properties, an interface can describe a callable value directly using call signature syntax: interface Comparator { (a: number, b: number): number; } declares that any value of type Comparator can be called with two numbers and returns a number — the interface itself has no named properties, just a call signature.

A single interface can even combine a call signature with regular properties, modeling a callable object that also carries extra data or configuration, such as a function with an attached .cache property — a pattern that comes up when adding memoization or metadata directly onto a function value.

In everyday code, a plain type alias like type Comparator<T> = (a: T, b: T) => number; is usually simpler and more common for pure function types; interface call signatures become useful specifically when the callable also needs named properties, or when you want the extensibility interfaces offer (see extension and merging below).

Extending interfaces and declaration merging

The extends keyword lets one interface build on another, inheriting all of its members: interface Employee extends Person { employeeId: number } produces an Employee that must satisfy everything Person requires, plus its own employeeId. An interface can extend multiple interfaces at once (interface A extends B, C {}), combining all their members, and can even extend a type alias as long as that alias resolves to an object shape.

Extension is the interface-specific counterpart to composing object shapes with intersections (type Employee = Person & Developer); the practical difference is that extends gives you clearer compiler error messages when a conflicting property type exists between the base and the extension, whereas an intersection of conflicting property types silently collapses into never for that property rather than erroring immediately.

Declaration merging is a capability unique to interfaces (and unavailable to type aliases): if you declare two interfaces with the same name in the same scope, TypeScript merges their members into a single combined interface rather than raising a duplicate-declaration error. This is intentional and is the mechanism libraries use to let consumers add properties to shapes they don't own — for example, augmenting a third-party library's Window or Request type by re-declaring interface Window { myGlobal: string } in your own code.

Declaration merging is powerful but easy to trigger by accident — naming two unrelated interfaces the same thing in a large codebase silently merges them instead of causing an error, which is one of the most-cited reasons some teams default to type for object shapes that aren't meant to be extended by others.

Interface vs. type alias: choosing the right tool

For a plain object shape, interface and type produce equivalent structural type checking — an object satisfies either one under exactly the same rules. The choice mostly comes down to which extra capabilities you need: reach for interface when the shape is meant to be extended by other interfaces, when you're describing something library consumers might want to augment via declaration merging, or when you're modeling a class's public contract (interfaces pair naturally with implements).

Reach for type when you need to alias anything that isn't an object shape — unions, intersections, tuples, primitives, mapped or conditional types — or when you want a name that is guaranteed not to silently merge with another declaration of the same name elsewhere in the codebase, which is often desirable for internal, non-public-API types.

In large, real-world codebases you will typically see both used side by side: interface for public, extensible object contracts (especially ones exposed as part of a library's API), and type for everything else, including unions, function signatures, and generic utility types. Neither choice is "wrong" for a simple object shape — consistency within a codebase matters more than which one you pick.

Interface declaration, extension, and call signature

Employee inherits id and name from User and adds its own members. greet(): string; is method-signature syntax, equivalent for most purposes to greet: () => string;. Comparator has no named properties — its only member is a call signature, making it a callable type.

interface User {
  id: number;
  name: string;
}

interface Employee extends User {
  readonly employeeId: number;
  greet(): string;
}

interface Comparator {
  (a: number, b: number): number;
}

Code Examples

Employee extends User, so an Employee object must satisfy every member of both interfaces: id, name, employeeId, and greet. TypeScript checks the object literal against the combined shape. Expected result: Hi, I'm Grace

Because both declarations use the name Config, TypeScript merges them into a single interface requiring both apiUrl and timeoutMs, rather than treating the second declaration as an error or an unrelated shadow. Expected result: Compiles successfully; Config effectively requires both apiUrl and timeoutMs.

Common mistake: Assuming a second interface Config { ... } declaration is a duplicate-identifier error, the way a second type Config = ... or class Config {} would be — interfaces merge instead of conflicting.

interface User {
  id: number;
  name: string;
}

interface Employee extends User {
  readonly employeeId: number;
  greet(): string;
}

const e: Employee = {
  id: 7,
  name: "Grace",
  employeeId: 4021,
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

console.log(e.greet());
// In one file (or module-level code):
interface Config {
  apiUrl: string;
}

// Later, elsewhere in the same scope, augmenting the same interface:
interface Config {
  timeoutMs: number;
}

const config: Config = {
  apiUrl: "https://api.example.com",
  timeoutMs: 5000,
};

Important Rules

Use extends to build interfaces on top of other interfaces (or object-shaped type aliases) when you want a clear "is-a-superset-of" relationship with good conflicting-property error messages.

Remember that two interfaces with the same name in the same scope merge automatically — this is a feature for augmenting library types, but an easy accidental bug in large codebases, so keep interface names specific.

Prefer interface for object shapes meant to be extended, implemented by classes, or augmented by consumers; prefer type for unions, intersections, function types, and anything not meant to merge.

Interface members are separated by semicolons or newlines, never commas.

Where this shows up

Public library APIs (including the TypeScript standard library's own DOM types) rely heavily on interface extension and declaration merging — for example, plugins for frameworks like Express or React commonly re-declare interface Request or interface Window to add their own properties into an existing type, which is only possible because interfaces merge rather than conflict.

interface User { id: number, name: string } interface User { id: number; name: string }

Interface members use semicolons (or just newlines) as separators, not commas — commas are valid in object literals but not in interface member lists, and using one is a straightforward syntax error.

[multiple-choice] What happens when you declare two interfaces with the same name in the same scope? A. TypeScript raises a duplicate-identifier error B. The second declaration silently shadows and replaces the first C. TypeScript merges their members into a single combined interface D. Only the properties common to both remain

C. TypeScript merges their members into a single combined interface — This is declaration merging, a capability unique to interfaces: same-named interface declarations in the same scope are combined into one interface requiring all members from every declaration.

[true-false] A type alias supports declaration merging the same way an interface does. A. True B. False

B. False — Declaration merging is exclusive to interfaces. Declaring a type alias twice with the same name in the same scope is a duplicate-identifier error, not a merge.

[multiple-choice] Which scenario is the strongest reason to choose interface over type for a given shape? A. The shape needs to alias a union of string literals B. The shape is a public contract meant to be extended by other interfaces or augmented via declaration merging C. The shape needs to alias a tuple D. The shape will never be used by any other code

B. The shape is a public contract meant to be extended by other interfaces or augmented via declaration merging — Interfaces are the better fit specifically when extension (extends) or declaration merging (letting consumers add to the shape) is needed — unions, tuples, and other non-object types can only be expressed with type.

Declare an interface Shape with a method area(): number. Declare Circle and Rectangle interfaces that each extend Shape and add their own specific properties (radius; width and height). Write one object literal for each that correctly implements area().

interface Shape { area(): number; } interface Circle extends Shape { radius: number; } interface Rectangle extends Shape { width: number; height: number; } const c: Circle = { radius: 3, area() { return Math.PI * this.radius * this.radius; }, }; const r: Rectangle = { width: 4, height: 5, area() { return this.width * this.height; }, };

Each interface's area() implementation should use that shape's own properties (radius for Circle, width * height for Rectangle).