type vs interface in TypeScript
type vs interface in TypeScript comes down to one question: are you describing an object shape, or naming any shape the compiler can build, including unions, primitives and tuples? Interfaces cover the first case and add two bonuses (declaration merging and a clean extends). Type aliases cover both cases but give up merging in return.
Theory
Why the choice is not purely stylistic
Both describe types. Both pass the same structural check. You can hand a User defined as interface to a function expecting a type User, so long as the shape matches. The real question is not "which is more powerful" but "which side effects do I want today".
interface has two things type does not: the compiler lets you redeclare the same name and merges the fields, and extends gives you an inheritance chain that tools and error messages render cleanly. type has several things interface does not: it can name a primitive, a tuple, a union, an intersection, a mapped type, a conditional type, or anything built from typeof and keyof.
How the compiler treats each
An interface is a named object type that the type checker keeps as a handle. Two declarations of the same interface merge into one handle with the union of their members. A member lookup resolves against that handle.
A type alias is a name for a type expression. The compiler substitutes the right side when it sees the name. Two type declarations with the same name are an error, because an alias cannot point at two different expressions at once.
The simplest direct comparison
interface User {
id: string;
email: string;
}
type UserAlias = {
id: string;
email: string;
};
const a: User = { id: "1", email: "a@b.c" };
const b: UserAlias = a; // works, structural matchFor a plain object shape the two are interchangeable. The compiler accepts an interface value where a type is expected, and the reverse, because TypeScript uses structural typing, not nominal.
Declaration merging is only available to interface
TypeScript lets you redeclare the same interface in the same scope and silently unifies the fields. Trying this with type is a duplicate identifier error.
interface Request {
id: string;
}
interface Request {
userId: string; // merged into Request
}
const r: Request = { id: "1", userId: "42" }; // both fields requiredThis is the whole reason library authors ship .d.ts files full of interface declarations for globals like Window or Express Request. A consumer can augment them from their own code without touching the library. On a TypeScript 5.2 project I worked on we standardized on interface for every request and response DTO, specifically because a contractor kept needing to patch Express Request with middleware fields. Three months later, zero declaration collisions.
Unions, intersections and primitives are type territory
type Id = string | number;
type Point = [number, number];
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: Error };None of these compile if you replace type with interface. An interface has to be an object shape. A discriminated union is the single most common pattern where type is not optional, and it is why reducers, API clients and form state libraries lean on type for their public surface.
Same check, different ergonomics
Put the mechanical difference in one line: both express object types, but interface is a handle the compiler keeps referencing, while type is an expression that gets inlined wherever it appears. That is why merging works for one and not the other, why extends produces nicer error messages for one and not the other, and why only one of them can name a union.
Rules for picking in practice
Use interface for object shapes and class contracts: DTOs, component props, service APIs, anything a class might implements. Use type for unions, tuples, primitive aliases, intersections, mapped types, conditional types, or anything derived with keyof or typeof. Do not switch styles in the middle of a project to look consistent, switch based on what the type actually is.
If you need declaration merging (global augmentation, library ambient types), interface is the only tool that does the job.
A common wrong assumption
Is interface slower than type, or are they really the same thing? Both questions come up on every other middle interview I sit in on. The short answer to the first is no, not in any way that matters for application code. The compiler caches aliased types per project, and the difference shows up only on very large monorepos with deeply nested generics. The short answer to the second is also no, they are not the same. They overlap for plain object shapes, but once you need a union, a primitive alias or a mapped type, only type compiles, and once you need declaration merging, only interface does the job.
Where this touches classes and libraries
A class can implement either one. class User implements UserType {} works the same as class User implements UserInterface {} from the class's point of view. The practical edge is that library consumers who need to augment a base contract from their own code can only do that with an interface. That is why NestJS, Express, Angular and most of the TS ecosystem publish public contracts as interfaces even when a type would have compiled fine. See the TypeScript handbook on object types for the language's own framing.
Examples
A DTO shared between a service and a test fixture
interface OrderDto {
id: string;
customerId: string;
totalCents: number;
createdAt: string;
}
function buildOrderFixture(overrides: Partial<OrderDto> = {}): OrderDto {
return {
id: "ord_1",
customerId: "cus_42",
totalCents: 1999,
createdAt: "2025-01-01T00:00:00Z",
...overrides,
};
}
const order = buildOrderFixture({ totalCents: 4500 });
// order.totalCents === 4500, other fields keep their defaultsOrderDto is a plain object contract, so interface is the natural fit. A future consumer can augment it (say, to add an audit field through a shared types package) without touching the original file. Partial<OrderDto> still works because the compiler treats interfaces as regular object types when building utility types.
An API client whose response is a discriminated union
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: { code: string; message: string } };
async function getOrder(id: string): Promise<ApiResult<OrderDto>> {
const res = await fetch(`/orders/${id}`);
if (!res.ok) {
return {
ok: false,
error: { code: "http_error", message: res.statusText },
};
}
return { ok: true, data: await res.json() };
}
const result = await getOrder("ord_1");
if (result.ok) {
console.log(result.data.totalCents); // narrowed to OrderDto
} else {
console.error(result.error.code); // narrowed to the error shape
}ApiResult cannot be written as an interface because its outer shape is a union, not a single object. The ok field is what makes the narrowing work: after the if, TypeScript knows which branch of the union you are in and exposes the fields for that branch only. Reach for type whenever the value you are typing has more than one legitimate shape. The rest of the code can still use interface for flat pieces like OrderDto, and both styles coexist in the same module without friction.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.