Every TypeScript codebase eventually has this argument: interface or type? The internet is full of oversimplified advice ("just use type for everything"), but the real answer is more nuanced. Interfaces and type aliases have genuinely different capabilities, different error messages, and different roles in a well-structured codebase. Here is how they actually differ, and when each one earns its place.
Master typescript interfaces vs type aliases
Take the TypeScript Essentials course with hands-on lessons and challenges.
When two interfaces declare conflicting properties, extends catches it immediately with a clear message pointing at the exact property. With type intersections, the conflicting property silently collapses to never, and you get a confusing error only when you try to assign a value — sometimes in a completely different file. This is the single strongest argument for preferring interface extends over type & for object hierarchies.
Declaration merging is the one capability that type aliases will never have. Anytime you need to augment a type from a third-party library — Express Request, Window, ProcessEnv, Prisma Client — you must use an interface. This alone justifies keeping interfaces in your toolkit.
Unions, discriminated unions, mapped types, and conditional types are the backbone of advanced TypeScript. None of them can be expressed as interfaces. If you banned type aliases, you would lose most of TypeScript's type-level expressiveness.
Component props are a textbook case for interfaces: they are object shapes, they form a hierarchy via extends, and downstream consumers may need to extend them. But the moment you need a union of those props or want to extract a single property type, you reach for a type alias. Both tools in the same file — that is normal.
When a class declares implements Logger, TypeScript enforces that every method exists with the correct signature. If you rename a method or change a parameter type, you get an immediate compile error in the class — not a silent runtime failure. This is the original purpose of interfaces and it remains their strongest use case in OOP-heavy codebases.
The TypeScript compiler treats interfaces as named, cached types. When you hover over Order in your editor, you see the interface name. With the intersection version, the compiler must flatten the intersection structurally each time it checks compatibility. In small projects the difference is negligible. In monorepos with hundreds of models, the interface version compiles noticeably faster.
Using `type` for every object shape "because it's simpler" — then struggling when you need to augment a library type or getting confusing `never` errors from intersection conflicts
Use interfaces for object shapes that form contracts or hierarchies. The extends keyword catches property conflicts at declaration time. Reserve type aliases for unions, tuples, mapped types, and type-level computation. Using both in the same file is not inconsistency — it is using the right tool for each job.
Attempting to use declaration merging with type aliases and getting confused by the "Duplicate identifier" error
Only interfaces support declaration merging. If you need to augment a third-party type, you must use `declare module` with an interface declaration. If you have a type alias you want to extend, either convert it to an interface or use intersection: `type Extended = Original & { newProp: string }`.
Extending an interface with `&` instead of `extends` — mixing the two composition mechanisms within the same type hierarchy
Pick one approach per inheritance chain. Interfaces use `extends`; type aliases use `&`. Mixing them works syntactically (`type Admin = User & { role: string }` where User is an interface), but it loses the clear error messages that `extends` provides on conflicts. If you started with interfaces, stay with `extends`.
Creating a union type with `interface` — trying to write something like `interface Status = 'active' | 'inactive'`
Interfaces cannot represent unions, tuples, or primitive types. Use a type alias: `type Status = 'active' | 'inactive'`. If you catch yourself needing something that is not an object shape, you need a type alias — there is no workaround with interfaces.
Interfaces and type aliases are complementary, not competing. Interfaces are purpose-built for object shapes: they support extends with clear conflict errors, declaration merging for augmenting third-party types, and implements for class contracts. Type aliases handle everything else: unions, tuples, mapped types, conditional types, and primitive aliases. The practical rule is straightforward — interfaces for object contracts, type aliases for type-level computation. Most production codebases use both, and that is the correct approach.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.