Ch. 2 · TypeScript

type vs interface: When It Actually Matters

Declaration merging, extends vs intersections, what only type aliases can express, and the few differences that change behavior, with a clear rule of thumb.

~5 min readbeginner

Every TypeScript interview seems to include “what’s the difference between type and interface?”, and the honest answer is: for describing the shape of an object, not much. Both can be generic, both can be extended, both can be implemented by a class. What interviewers want is the short list of places where the choice changes behavior, and a sensible rule for picking one. That list is shorter than most blog posts suggest.

Mostly, they’re interchangeable

Here is the same shape written both ways. Anywhere you use one, the other works.

interface UserI {
  id: string;
  name: string;
  greet(): string;
}

type UserT = {
  id: string;
  name: string;
  greet(): string;
};

// Both can extend the other kind
interface Admin extends UserT { permissions: string[] }
type Guest = UserI & { expiresAt: Date };
TypeScript

The differences fall into four buckets: declaration merging, how composition handles conflicts, what each can express, and a couple of subtle assignability and tooling details.

Declaration merging

Interfaces are open. Declare the same interface twice in the same scope and TypeScript merges the declarations into one. Type aliases are closed: a second declaration is an error.

interface User { id: string }
interface User { name: string }
const u: User = { id: "1", name: "Ada" }; // needs both fields

type Account = { id: string };
type Account = { name: string }; // ❌ Duplicate identifier 'Account'.
TypeScript

Merging is what makes augmentation possible: adding a field to a global like Window, or to an interface exported by a library, without touching its source.

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}
window.analytics.track("signup"); // typed

export {};
TypeScript

Gotcha

Merging can also happen by accident. In a script file (one with no import or export), declaring interface Comment { text: string } merges with the DOM’s global Comment type, and suddenly { text: "hi" } is missing dozens of properties.

If two merged declarations give the same property different types, that’s an error too (“Subsequent property declarations must have the same type”).

extends vs intersections

An interface composes with extends; a type alias composes with an intersection (&). They look equivalent until two parts disagree about a property.

interface Base { id: string }
interface Derived extends Base { id: number }
// ❌ Interface 'Derived' incorrectly extends interface 'Base'.

type BaseT = { id: string };
type DerivedT = BaseT & { id: number }; // no error here...
declare const d: DerivedT;
d.id; // ...but this is never (string & number)

type Pet = { kind: "cat" } & { kind: "dog" }; // the whole type is never
TypeScript

extends checks the new interface against the base and tells you right away. An intersection just combines the constraints, so a conflict silently produces never, either for that property or, when the conflicting properties are literal “discriminants” like kind, for the entire type. You find out later, somewhere far away, when nothing is assignable to it.

Narrowing is still fine with extends: interface Narrowed extends Base { id: "a" | "b" } compiles, because the new type is assignable to the old one.

What only type can do

A type alias can name any type. An interface can only describe an object type (including callable and constructable ones). So these need type:

type Status = "idle" | "loading" | "error"; // unions
type Id = string | number;
type Point = [x: number, y: number]; // tuples
type Flags = { [K in "dark" | "compact"]: boolean }; // mapped types
type ElementOf<T> = T extends readonly (infer U)[] ? U : never; // conditional
type Handler = `on${Capitalize<string>}`; // template literal types
TypeScript

There’s no interface spelling of a union, and the mapped-type syntax isn’t valid in an interface body. An interface can extend a mapped type when its keys are statically known, such as interface FlagsI extends Record<"dark" | "compact", boolean> {}, but it can’t extend a union: “An interface can only extend an object type or intersection of object types with statically known members.”

For how mapped and conditional types work, see Build TypeScript’s Utility Types From Scratch.

implements works with both

A class can implements an interface or a type alias, as long as the alias is an object type with known members. It can’t implement a union, for the same reason an interface can’t extend one.

type Named = { name: string };
interface HasAge { age: number }

class Person implements Named, HasAge {
  name = "Ada";
  age = 36;
}

type Shape = { kind: "circle"; r: number } | { kind: "square"; size: number };
class Circle implements Shape { kind = "circle" as const; r = 1; }
// ❌ A class can only implement an object type or intersection of object
//    types with statically known members.
TypeScript

Remember that implements only checks the class; it doesn’t add types to it. Members still need their own annotations.

Subtle differences that bite

Implicit index signatures. An object type written as a type alias is assignable to an index-signature type like Record<string, string>, because TypeScript can see all of its members. An interface isn’t, because it could be merged with more members later.

interface HeadersI { accept: string; host: string }
type HeadersT = { accept: string; host: string };

function send(headers: Record<string, string>) {}

declare const hi: HeadersI;
declare const ht: HeadersT;
send(ht); // OK
send(hi); // ❌ Index signature for type 'string' is missing in type 'HeadersI'.
TypeScript

This is the one that surprises people in real code, usually when passing an interface-typed object to a generic “dictionary” helper.

Error messages. Interfaces are always shown by name. Intersections are often reported by their anonymous pieces: a missing field in type Big = A & B & { c: boolean } can produce “required in type ‘{ c: boolean; }’”, while the interface version says “required in type ‘Big’”.

Checker performance. The TypeScript team’s performance guidance recommends interface ... extends over intersections for composing object types. An interface is a single flattened type whose relationships can be cached, while an intersection’s parts may need to be checked each time it’s compared. You’ll only feel this in large codebases with heavily composed types, but it’s a good detail to mention.

Interview tip

Mentioning the implicit index signature difference is a strong signal. Most candidates only know about declaration merging.

Which one should you use?

Use interface for object shapes, especially ones that other code extends, implements or augments, and compose them with extends. Use type for everything an interface can’t express: unions, tuples, function types, mapped, conditional and template literal types. This matches the TypeScript handbook’s heuristic of using interface until you need a feature of type.

Teams that use type everywhere aren’t wrong; consistency matters more than the choice. Just watch for intersections that should be extends.

interface type
Object shapes Yes Yes
Declaration merging Yes No (“Duplicate identifier”)
Composition extends, conflicts are errors &, conflicts become never
Unions, tuples, primitives No Yes
Mapped and conditional types Can extend one with known keys Yes
Used with implements Yes Yes, if an object type
Assignable to Record<string, T> No implicit index signature Yes
Shown in errors Always by name Intersections may show their parts

The interview answer

“For object shapes they’re nearly interchangeable: both can be generic, extended and implemented. The real differences are that interfaces support declaration merging, which is how you augment globals or library types, while a duplicate type is an error; that extends reports property conflicts while an intersection silently turns them into never; and that only type can express unions, tuples, mapped and conditional types. There’s also a subtle one: type aliases get an implicit index signature, so they’re assignable to Record<string, T> and interfaces aren’t.

My default is interface for object shapes and type for everything else, but I care more about the team being consistent.”

More in TypeScript

read ✓TypeScript · hard

Build TypeScript's Utility Types From Scratch

Reimplement Partial, Pick, Omit, Exclude, ReturnType, Awaited and friends with mapped and conditional types, plus modifiers, key remapping and template literals.

~6 min readread →
read ✓System Design · hard

Frontend System Design: Build an Autocomplete

A structured walkthrough of the autocomplete design round: requirements, architecture, race-free fetching, caching, rendering, the ARIA combobox and metrics.

~7 min readread →
esc