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). Atypecan't be reopened — declaring it twice is an error. - Extending: interfaces use
extends; aliases combine with an intersectionA & B, which means "has everything from both". If the two conflict,extendsreports an error at the declaration, while&silently produces an impossible property type such asstring & 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
typenames any type;interfaceonly 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 thanextends.