type vs interface in TypeScript
type vs interface in TypeScript - two ways to describe the shape of data, with one key difference: interface supports declaration merging across files, type does not.
Interviewers ask this question expecting you to name that difference specifically. "Both describe objects" is not enough at a middle or senior screen.
Theory
Why TypeScript has both
JavaScript had no type system. When TypeScript added one, it needed to support two coding styles at once: object-oriented code that extends and implements, and functional code that combines primitives, unions, and tuples. One construct could not cover both cleanly. So interface handles the OOP side, type handles everything else.
How interface works
An interface declares an object shape and gets a "mergeable" flag from the compiler. Write the same interface name in two different files, and TypeScript combines the members automatically. This is declaration merging.
interface User { name: string; }
interface User { age: number; }
// TypeScript reads this as: { name: string; age: number; }This is how @types/express works. To add a userId property to every Request object, you redeclare the interface in your own .d.ts file and it merges with the package definition automatically.
How type alias works
A type alias names a type expression: an object shape, a primitive, a union, an intersection, a tuple, or a conditional type. But once declared, you cannot reopen it.
type Status = "active" | "inactive"; // union
type ID = string | number; // primitive union
type Tagged<T> = T & { _tag: string }; // intersectioninterface Status = "active" | "inactive" is a syntax error. Interfaces are not union types.
What happens inside the compiler
- Parsing: Both
interface Person {}andtype Person = {}produce AST nodes. At this stage they look similar. - Binding: Interface symbols get a mergeable flag. Multiple declarations with the same name combine their members. Duplicate type aliases fail here with a compile error.
- Type checking:
interface Employee extends Personcreates a subtype relationship.type Employee = Person & { position: string }computes an explicit intersection. Both end with the same member set for objects. - Emission: Neither generates runtime code. Both disappear from compiled JavaScript completely.
Key differences
interface | type | |
|---|---|---|
| Object shape | β | β |
| Union / primitive | β | β |
| Conditional type | β | β |
| Declaration merging | β | β |
| Extending | extends | & |
Class implements | β | β |
interface extends with extends, the way a class hierarchy does. type intersects with &. Declaring the same type alias twice is an immediate compile error. Only type can alias a primitive, a union, a tuple, or a conditional type.
Where each belongs in production
Libraries expose interfaces. Applications use type aliases for most internal logic.
React and Chakra UI define interface Props in component files so consumers can extend them via declaration merging. Inside React Hook Form, UseFormReturn<T> is a type alias with conditional logic: T extends object ? { fields: T; reset: () => void } : never. An interface cannot express that shape.
I've seen codebases that use only type everywhere. It works, but you lose the ability to extend third-party definitions without creating wrappers.
Two misconceptions
The first: because both describe object shapes, they are interchangeable. Declaration merging breaks that:
type ID = { id: number };
type ID = { id: string }; // Error: Duplicate identifier 'ID'
interface ID { id: number; }
interface ID { id: string; } // Works: TypeScript merges to { id: number | string }The second: there is a runtime performance difference. There is not. Both erase completely to JavaScript. The choice has zero impact on bundle size or execution speed.
What to learn next
Declaration merging leads directly to module augmentation, which is how you add custom properties to Express.Request in real apps. Utility types like Partial<T> and Record<K, V> are built on type aliases, so understanding type first helps when studying utility types. Generics in TypeScript also become clearer once you see why type aliases exist.
Examples
Extending Express Request with interface merging
The most common real-world case for declaration merging. You want req.userId available in every route handler without a manual cast.
// types/express.d.ts
declare namespace Express {
interface Request {
userId?: string; // merged into Express.Request globally
}
}
// routes/profile.ts
const getProfile = (req: Express.Request, res: Express.Response): void => {
console.log(req.userId); // TypeScript knows this property exists
};The property is available everywhere because the interface merged. A type alias cannot do this. Without merging, you would create a separate AuthRequest type and cast to it manually in every handler.
Conditional return type with type alias
A function that returns different shapes depending on a generic parameter. Only type can express this.
interface FormField {
name: string;
value: string | number;
}
// Conditional type - interface cannot represent this
type UseFormReturn<T> = T extends object
? { fields: T; reset: () => void }
: never;
type LoginForm = { email: string; password: string };
const form = {} as UseFormReturn<LoginForm>;
// form.fields.email - TypeScript infers the exact shape
// UseFormReturn<string> resolves to never - caught at compile timeinterface FormField is correct here because other packages might extend it. type UseFormReturn is correct because it uses a conditional expression that interfaces cannot represent.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.