Intermediate4 questionsFull Guide

TypeScript Types vs Interfaces — Complete Interview Guide

Master the real differences between type aliases and interfaces in TypeScript — when to use each, how declaration merging works, which extends which, and how to answer every interviewer question on this perennial topic with confidence.

The Mental Model

Think of an interface as a named contract posted on a wall — any team member (object or class) can sign it, and you can add new clauses to it later (declaration merging). A type alias is more like a sticky note with a formula written on it — it can describe anything (unions, primitives, tuples, computed shapes), but once you stick it somewhere, you cannot add more to it. Both describe shapes; the difference is in flexibility, extensibility, and where each one shines.

The Explanation

The Core Distinction in One Sentence

Interfaces describe the shape of objects and classes and support declaration merging. Type aliases can describe any type — primitives, unions, tuples, mapped types, conditional types — and cannot be merged. For plain object shapes both work equally well; the differences matter at the edges.

Syntax Comparison

// Interface — keyword-based, feels like a contract
interface User {
  id: number;
  name: string;
  email?: string; // optional property
}

// Type alias — assignment-based, more like a named formula
type User = {
  id: number;
  name: string;
  email?: string;
};

// Both produce identical structural types — TypeScript treats them the same
// when checking compatibility between objects and these definitions.

What Only type Can Do

Type aliases can represent things interfaces simply cannot:

// Union types — impossible with interface
type Status = "active" | "inactive" | "pending";
type ID = string | number;

// Primitive aliases
type Milliseconds = number;
type UserName = string;

// Tuple types
type Coordinate = [number, number];
type NamedEntry = [string, ...number[]];

// Mapped types
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;

// Function type shorthand
type Handler = (event: Event) => void;
// (interfaces can describe call signatures too, but the syntax is clunkier)

What Only interface Can Do — Declaration Merging

You can declare the same interface name multiple times and TypeScript merges all declarations into one shape. This is declaration merging and it is the feature that enables library augmentation:

// First declaration — in your library's types
interface Window {
  myAnalytics: Analytics;
}

// Second declaration — in a consumer's augmentation file
interface Window {
  myFeatureFlag: boolean;
}

// TypeScript merges them — Window now has BOTH properties
// This is how libraries like @types/jest add methods to global Jest types

// Same trick works for extending third-party types:
declare module "express-session" {
  interface SessionData {
    userId: string;
    cart: CartItem[];
  }
}
Declaration merging is the primary reason to choose interface over type when writing library code or global augmentations. A type alias with the same name twice is a compile error.

Extending and Composing

Interfaces use extends. Type aliases use intersection (&). Both create a wider shape, but they behave differently when properties conflict:

// Interface extends — compile error on conflict
interface Animal {
  name: string;
}
interface Dog extends Animal {
  name: number; // Error: property 'name' in 'Dog' incompatible with 'Animal'
}

// Type intersection — silently produces 'never' on conflict
type Animal = { name: string };
type Dog = Animal & { name: number };
// Dog['name'] is string & number = never — no compile error here!
// This is a subtle and dangerous difference

// Interfaces extending type aliases — fully supported
type HasId = { id: number };
interface Product extends HasId {
  title: string;
  price: number;
}

// Type aliases intersecting interfaces — also fully supported
interface Named { name: string; }
type Employee = Named & { employeeId: string };

Class Implementation

Classes can implement both interfaces and type aliases that describe object shapes — but not unions or primitives:

interface Serializable {
  serialize(): string;
}

type Loggable = {
  log(): void;
};

class UserService implements Serializable, Loggable {
  serialize() { return JSON.stringify(this); }
  log() { console.log(this.serialize()); }
}

// This does NOT work — cannot implement a union
type StringOrNumber = string | number;
class Broken implements StringOrNumber {} // Error

Performance in Large Codebases

The TypeScript compiler can cache and reuse interface types more efficiently than complex type aliases in large projects. For simple object shapes the difference is negligible, but extremely complex intersection and conditional types can slow down type-checking in large monorepos. This is a secondary concern — choose the right abstraction first.

Common Misconceptions

⚠️

Many developers think interfaces and type aliases are completely interchangeable for object shapes — they are nearly identical for simple objects, but diverge on declaration merging (interfaces only), union types (type aliases only), and conflict handling in extends vs intersection (interfaces error; intersections silently produce never).

⚠️

Many developers think 'type is more modern so prefer it always' — the TypeScript team itself recommends interfaces for object shapes in library code because declaration merging enables consumers to augment library types without forking them.

⚠️

Many developers think you cannot extend a type alias with an interface — you can. interface Product extends HasId works even when HasId is a type alias. The extends keyword works across both constructs.

⚠️

Many developers think type intersection (&) always errors on conflicting properties like interface extends does — it does not. Conflicting property types are silently narrowed to never, which produces type-safe but often baffling 'this value is never assignable' errors downstream.

⚠️

Many developers think interfaces can describe union types — they cannot. type Result = Success | Failure is valid; interface Result = Success | Failure is a syntax error.

⚠️

Many developers think declaration merging only applies to global types — it applies to any interface, including module-level ones. This is how session augmentation, express Request extensions, and Jest global matchers are typed.

Where You'll See This in Real Code

Module augmentation in Express: teams extend the Request interface to add custom properties like req.user or req.session.cart. Because interfaces merge, you can add to the third-party definition without touching the library source.

API response shapes: most REST client code uses type aliases for discriminated unions like type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string } — this union pattern is impossible with an interface.

Design system component props: libraries like Radix UI and Chakra define component props as interfaces so consumers can use module augmentation or interface extension to add custom variants without forking the library.

Domain model types: id, price, and currency types are commonly aliased as type UserId = string or type Price = number to document intent and prevent mixing up unrelated string or number values across a codebase.

Global type extensions: @types/jest adds expect, describe, and it to the global scope by merging into NodeJS.Global — only possible because interfaces support declaration merging.

Interview Cheat Sheet

  • interface: use for object shapes, class contracts, and library code that consumers need to augment
  • type: use for unions, intersections, tuples, primitives, mapped types, and conditional types
  • Declaration merging: only interfaces — declaring the same interface name twice merges both declarations
  • extends (interface) vs & (type): extends errors on conflicting properties; & silently produces never
  • Classes can implement both interfaces and object-shape type aliases, but not union types
  • Both are erased at runtime — zero difference in compiled JavaScript output
  • interface extends type alias: fully supported — the extends keyword crosses both constructs
  • TypeScript team guidance: prefer interface for objects unless you need a feature only type provides
  • For function types: type Handler = (e: Event) => void is cleaner than a call-signature interface
💡

How to Answer in an Interview

  • 1.Open with the practical rule interviewers love: 'For plain object shapes, either works — I reach for interface when writing library code because declaration merging lets consumers augment types, and type when I need unions, tuples, or computed types that interfaces cannot express.'
  • 2.Know the declaration merging example cold — it's the single most important difference. Walk through the Express Request augmentation: 'You declare interface Request in your own file, TypeScript merges it with the library's Request, and suddenly req.user is typed everywhere without modifying node_modules.'
  • 3.The conflict behaviour difference is a senior-level signal: 'interface extends errors immediately on conflicting property types. Type intersection silently narrows conflicting properties to never — which compiles fine but produces mysterious downstream errors. This is a real footgun in large codebases.'
  • 4.When asked which to prefer, give a concrete rule and avoid 'it depends' without follow-through: 'I default to interface for objects and type for everything else. This aligns with the TypeScript handbook and plays nicely with external library augmentation.'
  • 5.Point out that both are erased at runtime — 'there is zero difference in the compiled JavaScript. This is a purely compile-time tool for developer experience and tooling.'

Practice Questions

4 questions
#01

typeof narrowing determines which branch runs

EasyType Inference
MicrosoftAtlassianGoogle+1
#02

What is type inference in TypeScript and when does it kick in?

EasyType System💡 TypeScript deduces types automatically from context — assignments, return values, default parameters
#03

What is the difference between any, unknown, and never in TypeScript?

EasyType System💡 any opts out of type checking; unknown is safe any (must narrow); never is the bottom type — unreachable code
#04

What is the difference between type and interface in TypeScript?

EasyType System PRO💡 interface is extendable and mergeable; type is more powerful (unions, tuples, mapped types) but not mergeable

Related Topics

TypeScript Union & Intersection Types — Complete Interview Guide
Intermediate·6–10 Qs
TypeScript Mapped Types — Complete Interview Guide
Advanced·6–10 Qs
TypeScript Generics — Complete Interview Guide
Intermediate·8–12 Qs
TypeScript Utility Types — Complete Interview Guide
Intermediate·6–10 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz