`type` Broke Library Augmentation - Use Interfaces
A production bug: type aliases block library augmentation, causing TypeScript errors and any casts.
- TypeScript's
typeandinterfaceboth define object shapes but serve different purposes. interfaceshines for domain entities — supportsextends,implements, and declaration merging.typeis your tool for unions, tuples, function signatures, and mapped types.- Performance difference is negligible at compilation; choosing wrong leads to maintenance debt.
- Biggest production mistake: using
typewhen you need declaration merging for library augmentation.
Think of a type or interface like a job description at a company. Before you hire someone, you write down exactly what skills and responsibilities that role requires — 'must know JavaScript, must handle billing, must have an email address.' TypeScript types and interfaces do the same thing for your data. They say: 'any object that wants to be a User must have a name, an email, and an age.' The difference between them is a bit like the difference between a sticky note (type) and an official HR form (interface) — both describe the role, but the HR form can be updated and extended department by department, while the sticky note is a fixed snapshot.
You've seen both type and interface in TypeScript codebases. They look interchangeable — both can describe an object's shape. But they aren't. Picking the wrong one causes real pain: you'll hit compile errors when trying to implements a type, or you'll be stuck when a third-party library expects to merge declarations and yours are sealed. Understanding the difference isn't academic trivia — it's the difference between ten-line workarounds and clean, maintainable types. This article lays out the concrete rules, the edge cases that bite you in production, and a decision framework you can use today.
Type Aliases — Snapshots of Any Shape You Can Imagine
A type alias does exactly what its name says: it gives a name to any type expression. That's broader than it sounds. You can alias a primitive, a union, a tuple, a function signature, or a complex object — anything TypeScript can express, you can name with type.
This is where type shines over interface: flexibility. An interface can only describe an object shape. A type can describe 'a string OR a number', a 'function that takes two numbers and returns a boolean', or even 'a tuple where position 0 is always a string and position 1 is always a Date.' You simply can't express those ideas with interface alone.
In a real codebase you'll use type aliases constantly for union types — things like an API response that can be either a success payload or an error object. You'll also use them to document function signatures so every developer on your team knows exactly what a callback or handler is supposed to look like before they write a single line.
type for unions prevents entire classes of bugs where invalid states slip through.type PaymentState = 'pending' | 'completed' | 'failed' makes it impossible to accidentally assign an invalid state.type when you need to describe what interface cannot — unions, tuples, primitives, function signatures.type is the only option for non-object shapes.Interfaces — Contracts That Can Grow and Merge
An interface describes the shape of an object and nothing else — but it does that job exceptionally well. Its real superpower is something called declaration merging: if you declare the same interface name twice, TypeScript merges them into one. This sounds niche until you realise it's the mechanism behind every third-party library that lets you extend their types from your own code, without touching the library source.
Interfaces also support extends, which works just like class inheritance. You can build a hierarchy of contracts — a BaseEntity with id and createdAt, extended by User which adds email, extended further by AdminUser which adds permissions. This makes your type system document your domain model in a way that mirrors how you'd describe it to a new teammate.
For almost any object shape that represents a domain entity — User, Product, Order, Invoice — interface is the idiomatic TypeScript choice. When you're describing something your app will create many instances of, or something that other parts of the codebase will extend or implement, interface communicates that intent clearly.
interface with a property that already exists but with a different type, you get an error. Worse, if you augment a third-party interface and make the property required, existing code that doesn't provide it will break. Always add new properties as optional (?).interface for objects others might extend — domain entities, component props, and class contracts.Intersection Types and Generics — Composing Complex Shapes
Once you understand types and interfaces individually, the real power comes from combining them. Intersection types (using &) let you merge multiple types into one that must satisfy all of them simultaneously. Think of it as 'this thing must be a User AND have these extra properties.' It's composition instead of inheritance.
Generics add a dimension of reusability that transforms your types from single-use descriptions into flexible templates. A generic type is like a function — it takes a type as a parameter and returns a new type. You've already seen this with ApiResponse<T> above. This is how TypeScript's own built-in utilities like Partial<T>, Required<T>, Pick<T, K> and Readonly<T> work under the hood.
In real-world code, you'll use these patterns every time you write shared utility functions, data fetching hooks, form handlers, or anything that needs to work with multiple different entity types. Getting comfortable with generics is the single biggest jump from 'TypeScript beginner' to 'TypeScript intermediate.'
PaginatedProduct, PaginatedUser, etc. When you later add a PaginatedOrder, you'd copy-paste. A single PaginatedResult<T> eliminates that. The performance impact is compile-time only — no runtime overhead.Partial<Pick<T, K>> is a production-grade pattern for selective updates.Type Guards — Making TypeScript Trust Your Runtime Logic
Here's a scenario that catches almost every developer moving from JavaScript to TypeScript: you have a variable typed as string | number — a union type. You want to call .toUpperCase() on it. TypeScript refuses, because what if it's a number? You need to prove to the compiler that at this specific point in the code, it's definitely a string.
Type guards are the mechanism for that proof. The simplest form is a typeof or instanceof check inside an if block — TypeScript understands these natively and narrows the type automatically inside that block. But when you're working with custom object types (not primitives), you need a user-defined type guard: a function that returns 'value is SomeType' in its signature.
This pattern is everywhere in production code — any time you receive data from an external source (an API, a user event, localStorage), you can't know the shape at compile time. Type guards are how you bridge the gap between 'unknown blob of JSON' and 'fully typed domain object.'
as assertions (lying to the compiler) or wrap everything in unsafe any. Both create runtime holes. A user-defined type guard costs a few microseconds per check — negligible compared to the safety gain.as assertions.When to Choose type vs interface: A Production Decision Guide
By now you know the capabilities of each. The real question is: which one do you actually write? The answer depends on the role that type plays in your codebase.
Start with interface for object shapes that represent domain entities — User, Product, Order. These are the backbone of your business logic. They benefit from extends, implements, and the ability for other modules to augment them. Interface communicates 'this is a contract'.
Use type for everything else: unions (Status = 'idle' | 'loading'), tuples ([lat, lng]), function signatures ((id: string) => Promise<User>), mapped types, utility types (Nullable<T>), and any composition involving intersections or generics. Type communicates 'this is a composition' or 'this is a variant'.
When in doubt, ask: 'Could another part of the system need to add a property to this?' If yes, interface. If no, type works fine. There's rarely a wrong choice for a pure object shape — both work — but interface gives you room to grow.
- Interface: like a legal contract — you sign it, and others can add clauses via declaration merging.
- Type: like a mathematical formula — it takes inputs (generics) and produces a single, immutable result.
- If you need to enforce a shape across your team and allow future extension, write an interface.
- If you need to express a complex transformation or a variant, write a type alias.
type for domain objects hits issues when they need to augment with analytics. A team that uses interface for everything is forced to write messy workarounds for unions. Establish a convention early: object contracts → interface, everything else → type.implements only works with interface.Partial<T>)The `type` That Broke Library Augmentation
any casts to silence the compiler.type and interface were equivalent for object types, so they standardized on type for consistency.type aliases are closed — they cannot be merged by subsequent declarations. The third-party library's module augmentation added properties to an interface, but the original type was invisible to it.type with an interface (no code change needed — same shape). The declaration merging worked immediately, and all any casts were removed.- Use
interfacefor any type that might need augmentation — especially public APIs and component props. typeis fine for internal unions, tuples, and utility types that don't need external extension.- If you're unsure, prefer
interfacefor object shapes; you can always use atypealias for the same shape later if needed.
class Foo implements MyType — TypeScript error: 'Only interfaces can be implemented'.MyType from type to interface. Classes can only implement interfaces.type via declaration merging gets 'Duplicate identifier' error.type to an interface. Only interfaces support declaration merging.kind: 'success' | 'error'), not a general string.interface – error: 'An interface can only extend an object type or intersection of object types'.type for unions. Interface cannot express string | number or {a:1} | {b:2}.type, replace with interface and keep the shape identical.Key takeaways
Common mistakes to avoid
3 patternsUsing `type` for class contracts
class UserService implements UserServiceType because classes can only implement interfaces, not arbitrary type aliases that contain union types or mapped types.Forgetting that declaration merging can cause unexpected behaviour
Using `as` type assertions instead of type guards to handle unknown data
const user = apiResponse as User and TypeScript stops complaining, but you get a runtime crash when the API returns unexpected data because you bypassed the type checker rather than validating the data.Interview Questions on This Topic
What's the practical difference between a type alias and an interface in TypeScript, and can you give a real scenario where you'd choose one over the other?
extends and implemented via implements; type aliases cannot be merged or implemented, but they can represent unions, tuples, and more complex type expressions. A real scenario: if you're defining the shape of a React component's props that other libraries might augment (like react-router's RouteComponentProps), use an interface. If you're writing a union type like type Result = 'success' | 'error', use a type.Frequently Asked Questions
That's TypeScript. Mark it forged?
4 min read · try the examples if you haven't