interface vs type in TypeScript — when to use each
Mostly interchangeable for object shapes. `interface` supports declaration merging and `extends` chains efficiently — prefer it for public API contracts and class implementations. `type` can express unions, intersections, tuples, mapped/conditional types, primitives — required for anything beyond plain objects. Pick one as the default for your codebase and stay consistent.
Both describe types. They overlap for plain object shapes. Each has things the other can't do.
What interface does that type doesn't.
1. Declaration merging.
interface Window { myCustom: string; }
interface Window { another: number; }
// Window now has bothUseful for augmenting third-party types (extending Window, Express.Request, etc.). type can't do this — type Window = ... twice is an error.
2. Faster (and clearer) for class implementations.
interface Repository { find(id: string): Promise<User>; }
class UserRepo implements Repository { ... }type works here too, but interface is the conventional pick.
3. extends produces better error messages and (historically) faster compile times for chained inheritance.
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }vs
type Animal = { name: string };
type Dog = Animal & { breed: string };Functionally the same. Errors at use sites typically point more clearly to the offending interface than to "intersection of Animal & {...}".
What type does that interface doesn't.
1. Unions.
type Status = "idle" | "loading" | "success" | "error";
type Result = { ok: true; data: T } | { ok: false; error: string };You cannot express a union with interface. This alone makes type essential.
2. Primitives, tuples, function types as named aliases.
type ID = string;
type Point = [number, number];
type Handler = (e: Event) => void;interface can describe call signatures but is awkward; type is cleaner.
3. Mapped types, conditional types, template literal types.
type Optional<T> = { [K in keyof T]?: T[K] };
type NonNull<T> = T extends null | undefined ? never : T;
type EventName<T extends string> = `on${Capitalize<T>}`;All type-only.
4. typeof extraction.
const config = { host: "x", port: 80 } as const;
type Config = typeof config;The pragmatic rule.
- Public API / library exports / class contracts →
interface. Consumers can augment via declaration merging if needed. - Anything that uses unions, conditional, mapped, template literal, or aliases a primitive/tuple/function →
type. - Internal object shapes → either. Many codebases just say "always
type" because the union case is too common. That's fine.
Subtle differences worth knowing.
extendschecking is structural, not nominal. Two interfaces with the same shape are interchangeable. TS has no nominal types out of the box (use branding to fake them).- Implements vs extends.
class Foo implements Barrequires Bar's shape but no inheritance.interface A extends Badds B's members to A. - An interface can extend a type alias and vice versa, as long as the alias is an object type.
- Recursive types.
``ts interface Tree { value: number; children: Tree[]; } // works type Tree = { value: number; children: Tree[]; }; // also works (since TS 3.7+) ``
The 2026 convention in popular codebases.
- React types in
@types/reactuseinterfacefor component props (extensibility). - Most schema libraries (Zod, Valibot) emit types via
type X = z.infer<...>. - Internal teams: pick one for objects and use
typefor the special features.typeeverywhere is the most common stance in new codebases.
Senior framing. The candidate who says "interface for objects, type for unions" is correct but shallow. The senior point: interface exists for declaration merging (a real superpower when augmenting third-party types), type exists because the type system needs algebra (unions, conditionals, mapped types). Use type as the default unless you specifically need merging or class implementation conventions.
Follow-up questions
- •What is declaration merging and when is it useful?
- •Can `interface` express a discriminated union?
- •Performance: is `interface extends` actually faster than `&` intersection?
- •How do you fake nominal types in TypeScript?
Common mistakes
- •Using `interface` for a union shape — it doesn't compile.
- •Trying to declare-merge a `type`.
- •Reaching for `interface extends` when you actually want `&` intersection of disjoint shapes.
Performance considerations
- •Deep nested `&` intersections can slow the type checker; `interface extends` chains are slightly leaner.
- •Excessive conditional types can blow up inference time — measure with `tsc --extendedDiagnostics`.
Edge cases
- •Augmenting `Window` or `globalThis` requires `declare global { interface Window { ... } }`.
- •Intersection of conflicting member types collapses to `never` — easy to miss.
- •`interface` can't represent `Record<K, V>` as cleanly as `type`.
Real-world examples
- •React component props in TypeScript starter templates — typically `interface`.
- •Redux slice types, API response unions — `type` with discriminants.