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.
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.
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.
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.
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 type05Open 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.
Try it yourself
- 1
Merge a third piece
In the first example add
interface Settings { language: string }and predict the error on theconst sline.TS2741appears:Property 'language' is missing ..., because merging madelanguagerequired. - 2
Swap the keyword
In the third example change
type Plain = { a: number };tointerface Plain { a: number }. Predict which function becomes an error.Both
fromAliasandfromInterfaceare now rejected: the alias was the only thing that fit an index signature. - 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: usetype.
Code & diagrams
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 }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 }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 }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
undefinedBreak 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 };.
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 }.
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
interfacewith aclassof the same name and anamespacewith 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 & Cwith an interface that extends all three can cut check time noticeably; usetsc --extendedDiagnosticsto measure before changing anything.
Remember this
- 1
The problem this solves: Two features can name an object shape,
typeandinterface. 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 }andinterface 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
Declaration merging. If you declare an
interfacewith the same name twice in the same scope, TypeScript merges the members into one interface. Atypedeclared twice is an error (TS2300: Duplicate identifier). Merging is what lets libraries be extended: you add a property to the built-inWindowor to Express'sRequestby declaringinterface Window { appVersion: string }in your own file (inside a module, meaning a file that has animportorexport, you wrap it indeclare 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
**What only
typecan 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/typeofresults, 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
**What only
interfacecan do.** Merge (above). Also,interface ... extendsgives 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
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 aninterfacethat extends several others gives a single named type. For performance, the TypeScript team's guidance is: preferinterface ... extendsover&, 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
A rule of thumb that works for teams. Use
interfacefor the public shapes of objects and classes that others may extend or augment (library and API types). Usetypefor 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 hasconsistent-type-definitions). The wrong choice rarely breaks code; inconsistent choices make a codebase harder to read.
Explain it without notes
What are the real differences between type and interface?
What is declaration merging, and when is it useful?
Why is an interface not assignable to { [k: string]: number } when an equivalent type is?
What rule would you give a team for choosing between them?
Practice
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.
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
interfaceeverywhere for objects lets consumers augment your types, but also lets surprising merges happen by accident. Usingtypeeverywhere 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
typecan express and one thing onlyinterfacecan 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
extendsis preferred to long&chains for large projects.I can state a team rule and defend it.