Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~22 min· topic 4 of 6

Topic 3.4

type or interface?

In one line

For plain object shapes type and interface do almost the same job. This topic covers the real differences: interfaces merge, aliases can name any type, and the two behave a little differently in errors and speed. It ends with a simple team rule.

Think of it like this

A paper form and a sticky note. A form (an interface) can be reprinted with extra boxes added by other departments: "Finance added a box, HR added a box", and they all end up on one form. A sticky note (a type alias) can hold anything you like, a shape, a choice, a list, but once written, nobody else adds to it. Interfaces can be added to later; aliases are sealed but can name anything.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Declaration merging
TypeScript combining several declarations of the same name into one. Interfaces merge; type aliases do not.
Augmentation
Adding members to a type declared elsewhere (a library or a built-in) by declaring the same interface again.
Mapped type
A type that builds a new object type by looping over the keys of another, like { [K in keyof T]: boolean }. Phase 8 covers it.
Open / closed
An interface is open to later additions through merging. An alias is closed once declared.
Index signature
A rule for any key, like [key: string]: number. Topic 3.6 covers it.
Duplicate identifier
Error TS2300: two declarations use a name that does not allow merging.
Linter rule
An automatic style check that teams run in the editor and in CI to keep code consistent.

Step by step

01Same job for plain shapes

Both of these accept exactly the same values. Pick one and stay consistent.

same.tswhole filets
type PointT = { x: number; y: number };

interface PointI {
  x: number;
  y: number;
}

02Interfaces merge

Two interfaces with the same name become one. Every member from both is required.

A type alias with a repeated name is a hard error on both declarations.

merge.tswhole filets
interface Settings { theme: string }
interface Settings { fontSize: number }
// Settings is now { theme: string; fontSize: number }

type Config = { debug: boolean };
type Config = { verbose: boolean };
// error TS2300: Duplicate identifier 'Config'.   (reported on both lines)

03Augment something you don't own

Merging is how you tell TypeScript about things added by other code. Declaring a property on Window makes window.appVersion type-check.

Inside a module (a file with import or export) wrap the interface in declare global { ... }, otherwise you'd only be declaring a new local Window.

global.d.tswhole filets
declare global {
  interface Window {
    appVersion: string;
  }
}
export {};

04Things only an alias can name

Unions, tuples, template literal types, keyof, mapped and conditional types are not object shapes, so an interface cannot be them.

This is why most real codebases use both keywords.

alias-only.tswhole filets
type Id = string | number;                       // union
type Pair = [string, number];                    // tuple
type Keys = keyof { a: 1; b: 2 };                // "a" | "b"
type Flags<T> = { [K in keyof T]: boolean };     // mapped type

05Open versus closed, and the index-signature rule

Because an interface may still grow (someone can merge more members into it), TypeScript will not let it be used where an index signature ("every property is a number") is required.

An alias with the same members is closed, so the checker can see all its properties and accepts it.

Open versus closed, and the index-signature rulediagram
Rendering diagram…

Try it yourself

  1. 1

    Merge a third piece

    In the first example add interface Settings { language: string } and predict the error on the const s line.

    TS2741 appears: Property 'language' is missing ..., because merging made language required.

  2. 2

    Swap the keyword

    In the third example change type Plain = { a: number }; to interface Plain { a: number }. Predict which function becomes an error.

    Both fromAlias and fromInterface are now rejected: the alias was the only thing that fit an index signature.

  3. 3

    Try to make an interface of a union

    In an empty editor try to write interface Id = string | number; and read what the editor says: TS1005: '{' expected, because an interface wants a body in braces, not =. There is no valid spelling for a union as an interface: use type.

Code & diagrams

Interfaces merge, aliases do not

The two Settings declarations became one, so the object needs both members. The two Config aliases are an error, reported on each line.

Console

(nothing was printed)

Expected console output

{ theme: 'dark', fontSize: 14 }
Only a type alias can name these

Try turning Id, Pair or Keys into an interface: there is no way to write them. The last two lines show the object-shape cases where either keyword works.

Console

(nothing was printed)

Expected console output

7 [ 'x', 1 ] a { x: true, y: false } { a: '1', b: 2 } { a: '1', b: 2 }
Why an interface is not an index-signature type

Plain and IRec have identical members, yet only the alias is accepted where an index signature is required. The interface could still be merged with more members later.

Console

(nothing was printed)

Expected console output

{ a: 1 }
Augmenting a built-in interface

Your declaration was merged into the browser's own Window interface. appVersion is known, appName is not. In a real project this goes in a global.d.ts file.

Console

(nothing was printed)

Expected console output

1.0
undefined

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Declare an alias twice

Write type Config = { debug: boolean }; and again type Config = { verbose: boolean };.

terminal
$ Types panel
── what you'll see ──
index.ts(12,6): error TS2300: Duplicate identifier 'Config'.
index.ts(13,6): error TS2300: Duplicate identifier 'Config'.

Break #2

Pass an interface where an index signature is required

Assign a value typed as interface IRec { a: number } to a variable of type { [k: string]: number }.

terminal
$ Types panel
── what you'll see ──
index.ts(12,9): error TS2322: Type 'IRec' is not assignable to type 'Rec'.
Index signature for type 'string' is missing in type 'IRec'.

Myth vs fact

Myth

Interfaces are always faster than type aliases.

Fact

For object shapes, interfaces that extend each other are cheaper than chains of & intersections. Plain object aliases and interfaces perform about the same, and unions and mapped types are only possible with aliases.

Myth

You should use only one of the two.

Fact

Both exist for good reasons. Most real codebases use interface for extendable object contracts and type for unions, functions, tuples and computed types.

Myth

Only interfaces can be implemented by classes.

Fact

A class can implement an object type alias too, as long as it is an object shape and not a union.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Declaration merging also merges an interface with a class of the same name and a namespace with a function, class or enum. The first is how some libraries add members to a class from outside; avoid it in application code because it confuses readers.

  • ▸

    When two merged interface declarations contain a method with the same name, they become overloads, and later declarations come first in overload resolution (except for string-literal parameter overloads, which are hoisted). This ordering rule is a well-known gotcha when augmenting library types.

  • ▸

    The performance guidance comes from how the checker caches work. Relationships between named interface types are cached after the first comparison. An intersection must be re-resolved property by property each time it is compared. In a huge monorepo, replacing A & B & C with an interface that extends all three can cut check time noticeably; use tsc --extendedDiagnostics to measure before changing anything.

Remember this

  1. 1

    The problem this solves: Two features can name an object shape, type and interface. Picking without a rule leads to confusing errors, or to a shape that others can't extend. So which one should you pick, and does it matter? For describing the shape of an object, they are interchangeable most of the time: type Point = { x: number; y: number } and interface Point { x: number; y: number } accept the same values and give the same errors for missing or wrong members. Any course that tells you one is "wrong" is overstating. The differences below are what actually matter.

  2. 2

    Declaration merging. If you declare an interface with the same name twice in the same scope, TypeScript merges the members into one interface. A type declared twice is an error (TS2300: Duplicate identifier). Merging is what lets libraries be extended: you add a property to the built-in Window or to Express's Request by declaring interface Window { appVersion: string } in your own file (inside a module, meaning a file that has an import or export, you wrap it in declare global). If you declare two members with the same name but different types, you get an error, and function members become overloads (several signatures for one function name, Topic 2.5).

  3. 3

    **What only type can do.** An alias can name types that are not object shapes: unions (string | number), tuples ([string, number]), primitives and literal types, template literal types, keyof/typeof results, conditional types, and mapped types ({ [K in keyof T]: boolean }). You meet the ones you have not seen yet in Phases 8 and 9; for now the point is that an interface cannot be written for any of them. An interface can only describe an object-like shape, though that shape can be callable, constructable or indexed. A class can implement an interface (class A implements Shape, Topic 6.5), and it can implement an object type alias too, so that is not a difference.

  4. 4

    **What only interface can do.** Merge (above). Also, interface ... extends gives a clear conflict error (TS2430, Topic 3.3), where & between aliases silently accepts the clash. And one subtle rule: an interface is not automatically assignable to a type with an index signature, while an alias with the same members is (the third example shows it). TypeScript treats interfaces as "open" (someone might merge in more members later), so it won't promise that all their properties fit an index signature.

  5. 5

    Errors and display. Both are printed by name in error messages and hovers. Intersections of aliases (A & B & C) are printed as written and can produce long messages, while an interface that extends several others gives a single named type. For performance, the TypeScript team's guidance is: prefer interface ... extends over &, because the compiler caches the relationships of interfaces by name, while it recomputes intersections. You won't notice this in a small project; in a very large one it shows in type-checking time.

  6. 6

    A rule of thumb that works for teams. Use interface for the public shapes of objects and classes that others may extend or augment (library and API types). Use type for everything else: unions, function types, tuples, mapped and conditional types, and local helper shapes. Whichever you pick, be consistent and let the linter enforce it (typescript-eslint has consistent-type-definitions). The wrong choice rarely breaks code; inconsistent choices make a codebase harder to read.

Explain it without notes

01

What are the real differences between type and interface?

02

What is declaration merging, and when is it useful?

03

Why is an interface not assignable to { [k: string]: number } when an equivalent type is?

04

What rule would you give a team for choosing between them?

Practice

01

Declare interface Todo { title: string } and then in another declaration add done: boolean. Create a todo object with both members and show what happens if you leave out done.

02

Write a type for a value that is either a number or a list of numbers, and a function total that adds it up. Explain why this can't be an interface.

Trade-offs

  • ↔

    Using interface everywhere for objects lets consumers augment your types, but also lets surprising merges happen by accident. Using type everywhere closes types and avoids accidental merges but makes augmentation impossible.

  • ↔

    Rules like "always interface" or "always type" are easy to enforce but sometimes force awkward code. A mixed rule (interface for object contracts, type for the rest) takes a sentence more to explain and fits real code better.

Done when you can

  • I can name three things only type can express and one thing only interface can do.

  • I can explain declaration merging and when it is used.

  • I can explain the open/closed difference behind the index-signature rule.

  • I can say why extends is preferred to long & chains for large projects.

  • I can state a team rule and defend it.