TypeScript Interface vs Type: Differences and Examples
TypeScript gives you two ways to name an object shape, and the internet has turned the choice into a bigger argument than it deserves. For most object shapes the two forms are interchangeable. Learn the few real differences, pick a default, and stay consistent.
This is the second guide in the types topic. It assumes you can read the object type syntax from the first types to learn.
How Do TypeScript Interfaces and Type Aliases Compare?
TypeScript interfaces and type aliases can describe the same object properties. Values with those properties can satisfy either declaration:
interface Product {
id: string;
name: string;
priceCents: number;
}
type ProductAlias = {
id: string;
name: string;
priceCents: number;
};
const book: Product = { id: "p1", name: "Refactoring", priceCents: 4200 };
const same: ProductAlias = book; // assignable both ways
console.log(same.name);
// prints: Refactoring TypeScript compares types by structure, not by name. Because both declarations describe the same structure, values flow freely between them, and both are erased at compilation. For a plain bag of properties, nothing observable changes when you swap one form for the other.
What Can a TypeScript Alias Name That an Interface Cannot?
A TypeScript alias can name a primitive, union, or tuple directly. An interface describes an object shape, including objects that can be called or indexed:
type ProductId = string;
type Pair = [x: number, y: number];
type CheckoutState =
| { status: "idle" }
| { status: "submitting"; cartId: string }
| { status: "failed"; message: string }; None of these three have an interface equivalent. A primitive alias, a tuple, and a union are all outside what interface can declare. The moment a shape needs to be "one of several alternatives", you are writing a type alias, and the unions and narrowing guide takes over from there.
How Do You Extend TypeScript Interfaces and Type Aliases?
TypeScript interfaces use extends; aliases use an intersection with &:
interface DiscountedProduct extends Product {
discountPercent: number;
}
type DiscountedProductAlias = Product & {
discountPercent: number;
}; They look equivalent, and usually are. The difference appears when the added properties conflict with the base. An extends clause that redeclares priceCents as a string is an error on the interface declaration itself, pointing at the exact line.
An intersection accepts the conflict silently. A clash like string and number produces a property of type never, which errors at construction or on an invalid operation rather than on every read. Conflicting literal discriminants go further and collapse the whole intersection to never.
That early, well-placed error is the strongest practical argument for extends when you are layering object shapes. It moves the failure to the declaration, where the mistake actually lives.
How Does TypeScript Interface Merging Work?
TypeScript merges interface declarations with the same name in the same scope into one combined shape, provided duplicated non-function members have identical types. Incompatible duplicates are an error, and duplicate methods stack as overloads with their own ordering rules. A duplicate type alias is simply an error:
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const prefs: Settings = { theme: "dark", fontSize: 14 };
console.log(prefs.fontSize);
// prints: 14
// with a type alias instead:
// error TS2300: Duplicate identifier 'Settings'. Inside one file, merging mostly looks like a footgun, and treating a duplicate as an error, as aliases do, is arguably saner. Accidentally declaring the same interface twice produces no warning at all, so long as the duplicated members agree in type.
Across files, merging is a feature the ecosystem leans on, though same-named interfaces in separate modules do not merge on their own. Applications augment framework types through module augmentation, a declare module block targeting the named export, or declare global for ambient types. Default exports cannot be augmented this way.
If you publish types that others should extend this way, that alone decides the question: expose an augmentable named interface, or a class whose instance side a merging interface can extend.
Should You Use a TypeScript Interface or Type Alias?
Use a type alias for unions, tuples, and primitives, and use an interface when callers need declaration merging. For ordinary object types, either works. The TypeScript documentation suggests using interface until you need features only aliases have, and interfaces also give clearer errors when extended properties conflict.
My own habit runs the other way. I use type aliases for state models, unions, and everything function-shaped, interfaces for public object contracts that might be extended or implemented. Either policy works. What does not work is a codebase where each file invents its own religion, because then every reader stops to wonder whether the difference in form encodes a difference in intent.
Whichever default you pick, the escape hatches stay the same. Reach for an alias when the shape is a union, tuple, or primitive, since interfaces cannot express those. Reach for an interface when external code must merge into the shape.
Pitfalls and Debugging
Duplicate identifier 'X'. If X is a type alias, you declared it twice, and one declaration must go. Check imports too: a local alias colliding with an imported name raises the related TS2440, Import declaration conflicts with local declaration, and renaming the import is usually the cleaner fix.
A property has become never after an intersection. Two intersected shapes declare the same property with incompatible types, and no value can satisfy both. The intersection itself never warned you. Find the conflicting property, fix one side, or switch to extends so the conflict errors at the declaration next time.
An interface has properties nobody declared. Almost always declaration merging: another declaration of the same name, often in a different file or a .d.ts, is contributing members. Editor tooling that jumps to a type's definitions will list every merged declaration site, which is the fastest way to find the contributor.
Interface 'X' incorrectly extends interface 'Y'. This is the good error, at the declaration line where the incompatible override lives. Compare the redeclared property's type with the base; the fix is either aligning the types or renaming the new property rather than silencing the error with a cast.
Frequently Asked Questions
Are TypeScript interfaces faster than type aliases?
TypeScript interfaces and type aliases have no runtime speed difference because both disappear during compilation. Compiler performance differences exist in very large codebases, where interfaces can cache better than complex intersections, but for application-scale projects the choice is about expressiveness and convention, not speed.
Can a TypeScript class implement a type alias?
A TypeScript class can implement an object type alias or an intersection with statically known members, but a normal class cannot implement a callable alias, and unresolved conditional types are rejected. It also cannot implement a union, because a class must be one concrete shape rather than one of several alternatives.
Can you convert a TypeScript interface to a type alias?
You can usually convert a TypeScript interface containing properties to an object type alias, or convert that alias back to an interface. The exceptions are aliases that use unions, mapped types, or conditional types, which have no interface equivalent, and interfaces that other code merges into, which an alias would break.
Why do TypeScript libraries expose interfaces?
TypeScript libraries expose interfaces when users need to extend their types. A published named interface can accept additional members through module augmentation, which is how projects add custom fields to framework types such as request objects or theme definitions. Library authors who want that extension point expose a named interface, or a class an interface can merge with, since aliases cannot merge.
Sources
-
[1]
Everyday Types(typescriptlang.org)
-
[2]
Object Types(typescriptlang.org)
-
[3]
Declaration Merging(typescriptlang.org)
Read Next
Primitive annotations, arrays, object types, and type inference in TypeScript: what to annotate, what to let the compiler infer, and why const and let behave differently.
Union types, literal types, and the narrowing toolkit: typeof, in, instanceof, discriminated unions, and exhaustiveness checking with never.
The TypeScript type system from the ground up: the first annotations, interfaces and type aliases, union types and narrowing, and handling unknown runtime data.