Interfaces Versus Type Aliases
Two ways to name a shape, what each can do that the other cannot, and a rule for choosing that will not start an argument in code review.
Two ways to name a shape, what each can do that the other cannot, and a rule for choosing that will not start an argument in code review.
You have been writing type Photo = { ... } for four lessons.
Then you open somebody else's code and find
interface Photo { ... } doing what looks like exactly the same
job, and a comment thread arguing about which is correct.
Both describe the shape of an object and they overlap almost entirely. By the end of this lesson you will know the handful of genuine differences, have a rule for choosing that will not start an argument, and — more usefully — understand the one behaviour of interfaces that can affect your code from a distance.
type Photo = {
name: string;
size: number;
};
interface Photo {
name: string;
size: number;
}Those are interchangeable in almost every way. Both check the same, both are erased at compile time, both work as parameter types, and both support optional and readonly properties:
interface Photo {
readonly id: string;
name: string;
caption?: string;
}Note the punctuation: an interface has no = and no closing
semicolon, because it is a declaration rather than an
assignment. That is the most common source of confusion when
switching between them.
A type alias names any type. An interface describes only the shape of an object.
type Status = "pending" | "done" | "failed"; // a union
type Id = string | number;
type Point = [number, number]; // a tuple
type Transform = (photo: Photo) => string; // a function
type Sizes = number[];
type PhotoName = Photo["name"]; // a lookupNone of those can be an interface. So the moment you want a union — which the previous lesson argued you want often — the question answers itself.
Combining is spelled differently in each:
type StoredPhoto = Photo & Timestamps; // intersection
interface StoredPhoto extends Photo, Timestamps {}extends reads better for building up object shapes and gives
clearer error messages when something conflicts. & works on
anything, including unions, where extends does not apply.
An interface can be declared more than once, and the declarations merge:
interface Photo {
name: string;
}
interface Photo {
size: number;
}
// Photo now has bothA type alias refuses:
type Photo = { name: string };
type Photo = { size: number };
// Duplicate identifier 'Photo'.This is declaration merging, and it exists for a specific purpose: extending a type you do not own. If a library declares an interface and you need to add a property that a plugin provides, you can declare the same interface again in your own code and the two combine.
That single behaviour is both why libraries define their public shapes as interfaces, and the strongest argument against using them everywhere else.
The reason merging exists.
A library declares an interface; a plugin adds a property to it. You declare the same interface in your own code and the two combine.
There is no other way to do this.
Silently accepted.
A stray interface Window { ... } extends the global
Window across the whole project, so a typo becomes a
property that exists everywhere rather than an error.
A duplicate type fails immediately, at the line you wrote.
Both are fine, teams argue about it, and the argument is not worth the time it takes. A rule that holds up:
The default.
And the only option for anything that is not an object shape — unions, tuples, function types, literals, lookups.
When merging is the point.
An object shape that is part of a public API, or one you specifically want other files to be able to extend.
Reaching for it becomes a deliberate act with a reason behind it, rather than a coin toss.
The opposite convention — interfaces for objects, types for everything else — is equally defensible and used by plenty of codebases. What matters is picking one and letting a linter enforce it, so it stops being a conversation in review.
Worth clearing up, because the word "interface" means something stricter in other languages.
An interface here is a description of a shape, not a contract something must announce it implements. Any value with the right properties satisfies it:
interface HasName {
name: string;
}
function greet(thing: HasName): string {
return `Hello, ${thing.name}`;
}
greet({ name: "Ana", extra: 1 } as HasName); // fine
greet(photo); // fine
greet(customer); // fineNothing declared a relationship to HasName. This is
structural typing, from the objects lesson: the checker asks
what a value has, not what it claims to be. It is why you can
describe a shape after the fact for code you did not write.
Classes can announce they implement one:
class StoredPhoto implements Photo {
name = "";
size = 0;
}implements is a check, not a requirement — it verifies the
class has everything Photo needs, and produces an error where
you wrote it rather than at every use site. Classes get their
own lesson later.
Bad — a name that describes the mechanism.
interface IPhoto {
name: string;
}
type PhotoType = {
size: number;
};Good — a name that describes the thing.
interface Photo {
name: string;
}
type UploadStatus = "pending" | "done";The I prefix is a convention borrowed from languages where an
interface and a class are genuinely different kinds of thing
that both need names. Here the type is erased and there is
nothing to disambiguate — so IPhoto costs a character on every
use and tells the reader that a keyword was used, which they can
see.
PhotoType is worse in the same way, and it is an easy habit to
fall into when a type and a value would otherwise clash. When
that happens, the better fix is usually to name the value
more specifically — defaultPhoto, photoSchema — since the
type is the more general concept.
THE SAME
both describe object shapes
both are erased at compile time
both support optional and readonly properties
both work as parameter and return types
type Photo = { ... }; = and a trailing semicolon
interface Photo { ... } no = and no semicolon
ONLY `type` CAN
type Status = "a" | "b"; unions <- the common one
type Point = [number, number]; tuples
type Fn = (x: A) => B; function types
type Sizes = number[];
type Name = Photo["name"]; lookups
combine with &
ONLY `interface` CAN
be declared TWICE and merge
which is how you extend a type you do not own
and why a stray `interface Window` extends the global one
silently, with the error appearing somewhere else
combine with `extends` - better messages for object shapes
CHOOSING
`type` by default, and for anything not an object shape
`interface` for a public API shape, or when you want merging
the opposite convention is equally fine
pick one, let a linter enforce it, stop discussing it
STRUCTURAL, NOT NOMINAL
nothing has to declare it implements an interface
any value with the right properties satisfies it
`class X implements Photo` is a CHECK, not a requirement
NAMING
Photo, not IPhoto, not PhotoType
the I prefix comes from languages where it disambiguates;
here there is nothing to disambiguate
if a type and a value clash, rename the VALUEYou can now read either form without wondering whether the
choice was meaningful, and you know the one behaviour —
merging — that has effects beyond the file it appears in. If
your project has no convention, type by default is the one
that requires the fewest decisions.
Next is Enums and Literal Types, which returns to the fixed
set of options from the unions lesson. There is a dedicated
enum keyword for exactly that job, it behaves unlike anything
else in the language, and the lesson explains why a union of
string literals is usually the better tool.
Before you move on, take a type alias you have written and try to express it as an interface. If it is a plain object shape you will succeed; if it is a union you will not. Doing that once makes the boundary between them concrete rather than a rule you half-remember.