type vs interface

Reviewed & published by Brayan K

An interface describes an object or class shape that can be extended and merged, while a type alias can name any type at all — including unions, tuples, and mapped types — so each is the better tool for a different job.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of two ways to define a recipe. An interface is a recipe card pinned to a shared board: anyone can pin extra cards under the same name (merging) and write "based on the pasta recipe, plus..." (extends). A type alias is a sticky note that can say anything — "today's special is soup OR salad OR sandwich" (a union) — but you can't keep adding to the same note later. Different tools, different strengths.

1. For Object Shapes, They're Twins

When all you need is to describe a plain object, type and interface are nearly identical. Both support optional ? members, readonly, methods, and index signatures, and both are completely erased at runtime. For most everyday object shapes, you could swap one for the other and nothing about the running code would change.

// For a plain object shape, these are equivalent:
interface UserI { name: string; age: number }
type      UserT = { name: string; age: number };

const a: UserI = { name: "Ada", age: 36 };
const b: UserT = { name: "Bo",  age: 28 };

// Both support optional and readonly members the same way:
interface Account { readonly id: string; nickname?: string }

// Rule of thumb: reach for 'interface' for public object/class shapes you may
// extend, and 'type' when you need unions, tuples, or other non-object types.
// 'type' and 'interface' both describe object shapes. For plain objects they
// are nearly interchangeable. In TS:
//   interface User { name: string; age: number }
//   type    User2 = { name: string; age: number }
// At runtime NEITHER exists — both are erased. We work with the values.

// Both describe the SAME runtime object:
const user = { name: "Ada", age: 36 };
console.log(user.name + " is " + user.age); // Ada is 36

// A method works the same whether the shape came from a type or an interface:
const account = {
  balance: 100,
  deposit(amount) { this.balance += amount; return this.balance; },
};
console.log(account.deposit(50)); // 150
console.log(account.balance);     // 150

2. Interface Superpowers: Merging & extends

Two things only interfaces do. Declaration merging: declare an interface twice with the same name and TypeScript combines the members — invaluable for augmenting types you don't own, like adding a property to the global Window. And extends lets one interface inherit another's members, modelling "is-a-plus-more" relationships cleanly.

// 1) Declaration merging — same name twice, members combine:
interface Box { width: number }
interface Box { height: number }
const b: Box = { width: 4, height: 3 }; // ✅ Box has BOTH now

// 2) extends — inherit one interface's members into another:
interface Animal { name: string; legs: number }
interface Dog extends Animal { breed: string }
const rex: Dog = { name: "Rex", legs: 4, breed: "Lab" };

// A 'type' CANNOT be re-declared (it's a name clash) and uses '&' instead of
// 'extends' to combine. Merging is unique to interfaces — handy for augmenting
// library or global types you don't own.
// INTERFACE superpower: declaration MERGING and 'extends'.
// In TS, declaring the same interface twice MERGES the members:
//   interface Box { width: number }
//   interface Box { height: number }   // Box now has BOTH
// And 'interface B extends A' inherits A's members.
// At runtime we just build one object that has all of them:

const box = { width: 4, height: 3 };          // satisfies a merged interface
console.log(box.width * box.height);          // 12

// 'extends' models inheritance of shape — a Dog "is an" Animal plus more:
const dog = { name: "Rex", legs: 4, breed: "Lab" };
console.log(dog.name + " has " + dog.legs + " legs"); // Rex has 4 legs

// Merging is great for AUGMENTING existing types (e.g. adding to Window):
//   interface Window { myFlag: boolean }   // adds to the global Window
// Only INTERFACES merge; a 'type' alias declared twice is a name-clash error.

3. Type Superpowers: Unions, Tuples & More

A type alias can name any type, not just object shapes. Unions (string | number), tuples ([string, number]), literal-union "menus," and advanced mapped or conditional types all require a type — an interface simply can't express them. This is the clearest dividing line: anything algebraic belongs to type.

// Things ONLY a 'type' alias can express:
type Id      = string | number;        // union
type Pair    = [string, number];       // tuple
type Status  = "active" | "paused";    // literal union
type Getters<T> = {                    // mapped type
  [K in keyof T as `get${string & K}`]: () => T[K]
};

// An interface can only describe an object/function/array shape — it cannot
// be a union or tuple. So: 'interface' for extendable object shapes,
// 'type' for everything algebraic (unions, tuples, mapped, conditional).
// 'type' superpower: it aliases ANY type, not just object shapes —
// unions, tuples, primitives, mapped/conditional types. Interfaces can't.
// In TS:
//   type Id      = string | number;       // a union — interface CANNOT do this
//   type Pair    = [string, number];      // a tuple — interface CANNOT do this
//   type Status  = "on" | "off";          // literal union

// Union alias: the value is one of several at runtime:
function format(id) {
  return typeof id === "string" ? id.toUpperCase() : "#" + id;
}
console.log(format("ab"));  // AB
console.log(format(7));     // #7

// Tuple alias: a fixed-length, fixed-order pair:
const pair = ["age", 36];   // type Pair = [string, number]
console.log(pair[0] + " = " + pair[1]); // age = 36

// Literal-union alias as a tidy set of options:
const status = "on";        // type Status = "on" | "off"
console.log("Status: " + status); // Status: on

🎯 Your Turn

A literal-union Direction is a classic type alias. Fill in the blank marked ___ with the right literal, then run it.

// 🎯 YOUR TURN — replace the ___ then press "Try it Yourself".
// In TS:  type Direction = "north" | "south" | "east" | "west";
// 'move' nudges a position based on a literal-union Direction.

function move(pos, dir) {
  if (dir === ___) return { x: pos.x, y: pos.y + 1 };   // 👉 the literal "north"
  if (dir === "south") return { x: pos.x, y: pos.y - 1 };
  return pos;
}

console.log(move({ x: 0, y: 0 }, "north")); // { x: 0, y: 1 }
console.log(move({ x: 0, y: 0 }, "south")); // { x: 0, y: -1 }

// ✅ Expected output:
//    { x: 0, y: 1 }
//    { x: 0, y: -1 }

Common Errors (and the fix)

Pro Tips

📚 Keep learning

Mini-Challenge: Discriminated Shape

No blanks this time — just a brief and a starting outline. Build the shape factory yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: model a shape with a discriminated union (a 'type' job).
// In TS this is a union of object shapes:
//   type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }
//
// Write makeShape(kind, size) that returns an object with:
//   - the 'kind'
//   - the size under the right field (radius for circle, side for square)
//   - an 'area' (π·r² for circle, side² for square)
//
// Call it for a circle of size 2 and a square of size 3.
//
// ✅ Expected output:
//    circle area 12.57
//    square area 9

// your code here

🎉 Lesson Complete

Practice quiz

For describing a plain object shape, type and interface are:

  • Nearly interchangeable
  • Completely different
  • Both unable to have optional members
  • Only usable for classes

Answer: Nearly interchangeable. For a plain object shape both support optional members, readonly, and methods, and both are erased at runtime.

Which feature is unique to interfaces?

  • Naming a union type
  • Declaration merging
  • Naming a tuple type
  • Aliasing a primitive

Answer: Declaration merging. Declaring the same interface name twice merges the members; type aliases cannot do this.

Which can ONLY be expressed with a type alias, not an interface?

  • An optional member
  • A readonly member
  • A union like string | number
  • A method

Answer: A union like string | number. An interface can only describe an object/function/array shape; unions and tuples require a type alias.

Declaring the same 'type' alias name twice causes:

  • A merge of the two
  • A duplicate-identifier error
  • An automatic union
  • Nothing at all

Answer: A duplicate-identifier error. Unlike interfaces, re-declaring a type alias with the same name is a duplicate-identifier error.

Which keyword lets one interface inherit another's members?

  • implements
  • merges
  • extends
  • infers

Answer: extends. interface Dog extends Animal inherits Animal's members into Dog.

To combine two type aliases into one shape you use:

  • The | operator
  • The & operator
  • The extends keyword
  • The keyof operator

Answer: The & operator. Type aliases combine shapes with the intersection operator &, where interfaces use extends.

At runtime, a type and an interface declaration:

  • Both still exist as objects
  • Are both erased
  • Only the interface survives
  • Only the type survives

Answer: Are both erased. Both are purely compile-time constructs and are erased, so they never affect the running output.

Declaration merging is especially useful for:

  • Augmenting library or global types you do not own
  • Speeding up arrays
  • Defining unions
  • Replacing classes

Answer: Augmenting library or global types you do not own. Merging lets you add members to types like the global Window without forking the library.

A practical default rule of thumb is to use interface for:

  • Unions and tuples
  • Public object/class shapes you may extend
  • Mapped types
  • Primitive aliases

Answer: Public object/class shapes you may extend. Reach for interface for extendable object/class shapes, and type for unions, tuples, and other algebraic types.

Can an interface be declared as a union of two object shapes?

  • Yes, with the | syntax
  • Yes, using extends
  • No, interfaces cannot be unions
  • Only if both shapes match

Answer: No, interfaces cannot be unions. interface X = A | B does not parse; unions must use a type alias.

Continue this course

Frequently asked questions

Should I use type or interface by default?

A common, practical rule: use 'interface' for object and class shapes that form a public API or might be extended, and use 'type' for everything else — unions, tuples, function types, mapped or conditional types, and quick aliases. Both are erased at runtime and are structurally compatible, so for a plain object shape the choice is mostly stylistic; consistency within a codebase matters more than the exact rule.

What is declaration merging and why does only interface have it?

If you declare an interface with the same name more than once, TypeScript merges all the declarations into one combined interface. This is intentional and powerful for augmenting types you don't own — for example adding a property to the global Window, or extending a library's types. Type aliases deliberately lack this: re-declaring a 'type' with the same name is a duplicate-identifier error, which keeps aliases predictable.

Can a type alias do everything an interface can?

For object shapes, almost — a type alias supports optional members, readonly, methods, and index signatures, and it combines shapes with '&' instead of 'extends'. The one thing it cannot do is declaration merging. Conversely, an interface cannot be a union, tuple, or other non-object type. So neither is a strict superset; they overlap heavily for objects and diverge at the edges.

Is there a performance difference between type and interface?

For large, frequently-extended object shapes, interfaces can be marginally faster for the compiler because it can cache the relationship between an interface and its bases, whereas a complex intersection of type aliases may be recomputed. In everyday code the difference is negligible, but it's a minor reason teams favour interfaces for big, public, extendable object types.