A model starts as a simple object, then gains variants, shared fields, library extensions, or generated properties. That is usually when the TypeScript interface vs type decision stops feeling cosmetic.
The practical answer is straightforward: use an interface for an object contract designed to be extended or merged. Use a type alias when you need a union, tuple, primitive alias, mapped type, conditional type, or another composition that isn’t a single object shape. For ordinary objects, either is valid, so consistency matters more than ideology.
TL;DR
interface and type can both describe object shapes, support generics, extend other object types, and be implemented by classes. Their main differences concern what they can name and how they evolve.
Choose interface for extendable object-oriented contracts and public library APIs where declaration merging is intentional. Choose type for unions, intersections, tuples, mapped types, conditional types, and closed application models. There is no runtime performance difference because both disappear during compilation.
Table of Contents
TypeScript Interface vs Type: The Practical Difference
Interface vs Type Comparison
How Extension and Composition Behave
Real-World Scenarios
When to Use Interface or Type
Common Mistakes and Overlooked Trade-offs
Key Takeaways
A Practical Default
Frequently Asked Questions
TypeScript Interface vs Type: The Practical Difference
An interface names an object contract:
interface User {
id: string;
name: string;
email?: string;
}
A type alias gives a name to any valid TypeScript type expression:
type User = {
id: string;
name: string;
email?: string;
};
type UserId = string;
type Status = "active" | "inactive";
type Coordinates = [number, number];
For the two User definitions, assignment and property checking work almost identically. TypeScript uses structural typing, which means a value is compatible when it has the required structure; it doesn’t need to explicitly declare that it implements User.
The difference appears when the model is no longer just one object. A type alias can name a primitive, union, tuple, intersection, conditional type, or mapped type. An interface primarily describes an object-like structure with properties, methods, call signatures, or index signatures. The TypeScript Handbook’s comparison identifies reopening as the key distinction: interfaces can participate in declaration merging, while a type alias cannot be redeclared.
All examples below use syntax supported in TypeScript 5.x and can be checked in the TypeScript Playground.
Interface vs Type Comparison
Capability |
|
|
|---|---|---|
Describe an object | Yes | Yes |
Describe primitives | No | Yes |
Create unions | No | Yes |
Create tuples | No | Yes |
Extend object contracts | With | Usually with |
Extend multiple object types | Yes | Yes, through intersections |
Declaration merging | Yes | No |
Mapped and conditional types | Not directly | Yes |
Use generics | Yes | Yes |
Class implementation | Yes | Yes, for compatible object types |
Runtime performance difference | None | None |
Typical fit | Extensible object contracts | Type composition and transformations |
This table is a decision aid, not a rule that every codebase must follow. A simple object type doesn’t become incorrect because it uses type, and a union can still contain interfaces as its members.
How Extension and Composition Behave
Extending interfaces and intersecting type aliases
Interfaces use extends to build a new object contract from one or more existing contracts:
interface Timestamped {
createdAt: string;
updatedAt: string;
}
interface Identified {
id: string;
}
interface User extends Timestamped, Identified {
name: string;
}
The equivalent type alias normally uses an intersection:
type Timestamped = {
createdAt: string;
updatedAt: string;
};
type Identified = {
id: string;
};
type User = Timestamped &
Identified & {
name: string;
};
Both versions require User to contain all the specified properties. Interfaces can also extend compatible object type aliases, provided their members are statically known:
type Identified = {
id: string;
};
interface Customer extends Identified {
company: string;
}
An interface cannot extend a union because a union describes alternatives rather than one fixed collection of properties:
type RequestState =
| { status: "loading" }
| { status: "success"; data: string };
// Invalid: a union does not have statically known members
interface ExtendedState extends RequestState {}
The official documentation provides further examples of extending single and multiple object types.
Conflicting properties produce different feedback
Interface extension detects incompatible inherited properties immediately:
interface StringId {
id: string;
}
interface NumericId {
id: number;
}
// Error: the inherited "id" properties are incompatible
interface RecordId extends StringId, NumericId {}
An intersection is permitted, but the conflicting property becomes never:
type StringId = { id: string };
type NumericId = { id: number };
type RecordId = StringId & NumericId;
// RecordId["id"] is never
This difference matters in larger models. An interface often reports the conflict near the declaration, while an intersection may remain valid until someone tries to construct or use the impossible value.
Why unions are only available with type aliases
A union is a type expression representing one of several possible values. A type alias can name that complete expression:
type PaymentResult =
| { status: "paid"; transactionId: string }
| { status: "failed"; reason: string };
An interface declaration defines one object contract, so it cannot represent “this shape or that shape.” Interfaces can still serve as union members:
interface Paid {
status: "paid";
transactionId: string;
}
interface Failed {
status: "failed";
reason: string;
}
type PaymentResult = Paid | Failed;
The alias is required to join the alternatives. The interfaces are optional.
Declaration merging makes interfaces reopenable
Two compatible interface declarations with the same name merge:
interface RequestContext {
requestId: string;
}
interface RequestContext {
userId?: string;
}
const context: RequestContext = {
requestId: "req-101",
userId: "user-42",
};
This supports intentional library augmentation, including adding properties to framework or platform types. The declarations must remain compatible; conflicting non-function members cause an error. Type aliases cannot be reopened:
type RequestContext = {
requestId: string;
};
// Error: duplicate identifier
type RequestContext = {
userId?: string;
};
The declaration-merging documentation explains the merging and overload-order rules in detail.
Mapped and conditional types require aliases
A mapped type creates properties by iterating over another type’s keys:
type BooleanFields<T> = {
[Key in keyof T]: boolean;
};
interface User {
name: string;
email: string;
}
type UserFieldState = BooleanFields<User>;
// { name: boolean; email: boolean }
Conditional types also operate as type expressions:
type IdOf<T> = T extends { id: infer Id } ? Id : never;
type UserId = IdOf<{ id: string; name: string }>;
// string
These transformations need type aliases because an interface body cannot directly express mapped or conditional logic. See the official guide to mapped types and mapping modifiers.
Real-World Scenarios
React props: combine both instead of choosing a side
A simple, extendable props object works well as an interface:
interface CommonButtonProps {
children: React.ReactNode;
disabled?: boolean;
}
Once the component has mutually exclusive variants, a discriminated union is safer:
type ButtonProps = CommonButtonProps &
(
| {
variant: "link";
href: string;
onClick?: never;
}
| {
variant: "button";
href?: never;
onClick: () => void;
}
);
This hybrid model prevents a link variant without an href and prevents a button variant from receiving link-only properties. Using one declaration style everywhere would make the model less precise.
API responses: interfaces for entities, types for outcomes
API entities are usually stable object shapes:
interface User {
id: string;
name: string;
}
The request outcome is a choice between states, so a type alias is more appropriate:
type ApiResult<T> =
| { ok: true; data: T }
| {
ok: false;
error: {
code: string;
message: string;
};
};
function displayUser(result: ApiResult<User>) {
if (result.ok) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
}
Checking result.ok narrows the union, so each branch exposes only the valid properties. This models the API more accurately than one interface containing several optional fields.
Library design: decide whether extension is intentional
An exported interface is useful when consumers are expected to extend or augment a contract. Plugin options, framework contexts, and platform declarations are common examples.
For a closed application model, declaration merging may be unnecessary or even surprising. A type alias communicates that the name itself cannot be reopened, although it does not make the underlying values immutable or nominally typed.
The important library-design question is therefore not “Which syntax is shorter?” It is “Should downstream code be able to add to this declaration?”
When to Use Interface or Type
When to Use Interface Checklist
☐ The declaration represents one object contract.
☐ Consumers are expected to extend or implement it.
☐ Declaration merging or module augmentation is intentional.
☐ Several large object contracts are being combined through extends.
When to Use Type Checklist
☐ The model is a union, tuple, primitive alias, or function type.
☐ Mutually exclusive states must be represented precisely.
☐ The declaration uses intersections, mapped types, or conditional types.
☐ The name should not be reopened through declaration merging.
For simple internal objects where neither checklist clearly wins, follow the project’s convention.
Common Mistakes and Overlooked Trade-offs
Treating either declaration as runtime validation
Interfaces and type aliases only participate in static checking. TypeScript removes them during compilation, so neither validates JSON received from an API, form input, local storage, or third-party data at runtime.
External data still needs runtime validation before it is trusted. The compiler can check what your code claims a value is; it cannot prove that a server actually returned that shape.
Forcing every model into one team-wide rule
“Always use interfaces” prevents natural union and transformation patterns. “Always use types” gives up useful interface extension and merging. A reasonable style guide can establish a default, but it should include exceptions based on the model.
Assuming type aliases create distinct primitive types
This alias does not create a new nominal type:
type UserId = string;
type OrderId = string;
let userId: UserId = "user-1";
let orderId: OrderId = userId; // Allowed
Both aliases still represent string. Preventing accidental interchange requires a branding pattern or another nominal-typing technique.
Believing interfaces are always faster
There is no runtime difference: interfaces and type aliases are erased from emitted JavaScript. Compiler performance is more nuanced. For large object compositions, the TypeScript team’s performance guidance recommends interfaces with extends because relationships can be cached and property conflicts are detected more directly.
That does not mean replacing every small object alias will produce a measurable improvement. Choose clarity first, and investigate type-checking performance when a real project shows slow builds or editor feedback.
Key Takeaways
Both declarations work well for ordinary object shapes.
Interfaces are strongest for extendable, mergeable object contracts.
Type aliases are required for unions and type-level transformations.
Intersections can hide incompatible properties by reducing them to
never.Neither option changes runtime behavior or validates external data.
A Practical Default
Use interface for public object contracts and type for unions, transformations, and application state. Allow exceptions when the model requires them.
The strongest TypeScript designs often combine both: interfaces describe stable entities, while type aliases compose those entities into variants and derived types. Continue with the companion TypeScript Generics Guide and TypeScript Utility Types Explained to build reusable transformations without making types difficult to maintain.
Frequently Asked Questions
Is an interface better than a type in TypeScript?
Neither is universally better. An interface is usually clearer for an extendable object contract, while a type alias is necessary for unions, tuples, mapped types, and conditional types. For a simple internal object, both are valid, so use the convention that makes the codebase consistent.
Can a TypeScript type extend an interface?
A type alias can combine an interface with another object type by using an intersection:
interface Person {
name: string;
}
type Employee = Person & {
employeeId: string;
};
An interface can also extend a compatible object type alias, provided that alias has statically known members.
Can a class implement a type alias?
A class can implement a type alias when the alias describes an object type with statically known properties:
type Printable = {
print(): void;
};
class Report implements Printable {
print() {
console.log("Printing report");
}
}
A class cannot implement a union because it cannot guarantee one fixed member structure.
Why can’t a TypeScript interface be a union?
An interface declares one object structure, whereas a union is an expression representing alternative types. Use a type alias to join several interfaces or object types with the | operator. Each individual member may still be declared as an interface.
Do interface and type affect JavaScript performance?
No. TypeScript erases interfaces and type aliases during compilation, so neither exists in the generated JavaScript. Differences can appear in type-checking performance for complex projects, especially when comparing interface inheritance with deeply nested intersections, but they do not affect application runtime speed.
Should React props use interface or type?
Use an interface for straightforward props that form one extendable object shape. Use a type alias when props contain mutually exclusive variants or unions. Many React components benefit from combining an interface for shared properties with a type alias for variant-specific rules.
Can an interface use mapped types?
An interface cannot directly declare a mapped type with syntax such as [Key in keyof T]. Define the transformation with a type alias instead. An interface may sometimes extend the resulting object type when its members are statically known, but the mapping operation itself belongs in a type alias.




