Type aliases vs interfaces

TypeScript · Practical · 10 min

Teams argue about this constantly; knowing the real differences lets you pick quickly and consistently.

Both type and interface can name an object shape, and for plain objects they are nearly interchangeable:

interface Point { x: number; y: number }
type PointT = { x: number; y: number };

The real differences:

  • Only `type` can name non-object types: unions (type Id = string | number), literals, tuples, function types, and the results of mapped or conditional types.
  • Only `interface` supports declaration merging: two interface Window { ... } declarations combine into one. That's how libraries let you add fields to their types (module augmentation, a later lesson). A type can't be reopened — declaring it twice is an error.
  • Extending: interfaces use extends; aliases combine with an intersection A & B, which means "has everything from both". If the two conflict, extends reports an error at the declaration, while & silently produces an impossible property type such as string & number (that is, never) that you only discover later.
  • Performance and messages: the compiler caches interface relationships by name and shows interface names in errors and hovers, while big intersections can be slower to check and print as long expanded types.

A common, defensible convention: use interface for object shapes (especially public, extendable ones) and type for everything else — unions, tuples, functions, and computed types. Another equally valid one is "type everywhere unless you need merging". Pick one per codebase and let a lint rule (e.g. @typescript-eslint/consistent-type-definitions) enforce it.

A class can implements either form, as long as the alias describes an object type.

Example

interface Animal { name: string }
interface Dog extends Animal { bark(): string }           // interface extends

type Timestamps = { createdAt: Date; updatedAt: Date };
type Post = { title: string } & Timestamps;              // intersection

type Id = string | number;                                // only `type` can do this

// Interfaces merge; aliases can't be redeclared
interface Config { debug: boolean }
interface Config { verbose: boolean }
const cfg: Config = { debug: true, verbose: false };

// Conflicts: extends errors early, & yields never later
type A = { id: string };
type B = { id: number };
type AB = A & B;                 // id: never — no value can satisfy it
// @ts-expect-error -- Type 'string' is not assignable to type 'never'
const ab: AB = { id: "x" };

const rex: Dog = { name: "Rex", bark: () => "woof" };
const post: Post = { title: "Hi", createdAt: new Date(), updatedAt: new Date() };
const id: Id = 7;
console.log(rex.bark(), post.title, cfg.verbose, id);

Remember

  • type names any type; interface only object shapes
  • Interfaces merge and extend; aliases combine with &
  • Conflicting intersections quietly become never
  • Pick a convention and enforce it with a lint rule

Watch out for

  • Building large models out of many & intersections, which gives worse errors and slower checks than extends.

Check yourself

  1. You need a name for "a string or an array of strings". Which is possible?
  2. Why can a library's users add properties to an interface the library exports, but not to an exported type alias?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces