Skip to main content
Practice Problems

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.

typescript
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.

typescript
type Status = "active" | "inactive"; // union type ID = string | number; // primitive union type Tagged<T> = T & { _tag: string }; // intersection

interface Status = "active" | "inactive" is a syntax error. Interfaces are not union types.

What happens inside the compiler

  1. Parsing: Both interface Person {} and type Person = {} produce AST nodes. At this stage they look similar.
  2. 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.
  3. Type checking: interface Employee extends Person creates a subtype relationship. type Employee = Person & { position: string } computes an explicit intersection. Both end with the same member set for objects.
  4. Emission: Neither generates runtime code. Both disappear from compiled JavaScript completely.

Key differences

interfacetype
Object shapeβœ…βœ…
Union / primitiveβŒβœ…
Conditional typeβŒβœ…
Declaration mergingβœ…βŒ
Extendingextends&
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:

typescript
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.

typescript
// 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.

typescript
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 time

interface 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems