Coding tips

TypeScript interface vs type: Differences, Declaration Merging and When to Use Each (Tested Examples)

Interfaces and type aliases both describe object shapes, but only interfaces merge and only type aliases can name unions, tuples and mapped types. Here are the real differences, with compiler errors from TypeScript 7.0.2 and 5.9.3, and a simple rule for choosing.

By 13 min read 4 views
Cover image for TypeScript interface vs type: Differences, Declaration Merging and When to Use Each (Tested Examples)

In TypeScript, interface and type are interchangeable for most object shapes. The real differences are these. Interfaces can be declared more than once and merge (declaration merging), and extends reports property conflicts. Type aliases can name anything, including unions, tuples, primitives, mapped and conditional types, but they can't be reopened. A good default: use interface for object shapes and public APIs, and type for unions and computed types.

The TypeScript Handbook section on differences between type aliases and interfaces gives the short version. This guide shows each difference with code I compiled myself, including the exact errors you'll see when you hit the edges.

I compiled every example with TypeScript 7.0.2 (the new native compiler) and 5.9.3 using --strict, on Node.js 24.22.1. Both versions printed identical error messages. The only difference was the exit code: 1 on 7.0.2 and 2 on 5.9.3.

In this guide:

  • interface vs type at a glance
  • Where they behave the same
  • 1. Declaration merging: only interfaces can be reopened
  • The accidental merge trap
  • 2. extends vs intersection: how conflicts are reported
  • 3. Unions, tuples, mapped and conditional types need type
  • 4. implements works with both, with one catch
  • 5. You can mix them freely
  • 6. Implicit index signatures: a surprising difference
  • 7. Error messages: interfaces keep their names
  • 8. Performance: prefer interfaces for composition
  • Which should you use?
  • How I tested these examples
  • Frequently asked questions

interface vs type at a glance

Featureinterfacetype
Describe an object shapeYesYes
Function and method signaturesYesYes
Declaration merging (reopen it later)YesNo: Duplicate identifier error
Extend or combineextends, conflicts reported&, conflicts become never
Unions, tuples, primitivesNoYes
Mapped, conditional, template literal typesNoYes
Used with implementsYesYes, if it's an object type (not a union)
Assignable to Record<string, string>No implicit index signatureYes
Name kept in error messagesConsistentlyCan expand into the full structure
Checking speed for big compositionsFaster (cached)Intersections can be slower

Where they behave the same

For a plain object shape, the two forms are fully interchangeable. TypeScript compares types by structure, not by name, so a value of one fits the other:

interface UserI {
  id: number;
  name: string;
}

type UserT = {
  id: number;
  name: string;
};

const a: UserI = { id: 1, name: 'Mahir' };
const b: UserT = a; // fine: TypeScript compares shapes, not names
const c: UserI = b; // also fine

This compiled with no errors on both versions. Function types work either way too:

interface FormatI {
  (value: number): string;
}
type FormatT = (value: number) => string;

const toGBP: FormatI = (v) => `£${v.toFixed(2)}`;
const toUSD: FormatT = (v) => `$${v.toFixed(2)}`;
$ node t12_functions.ts
£4.50 $4.50

Node 24 ran the .ts file directly with type stripping, with no build step. Type stripping only removes annotations and never type-checks, so you still need tsc to catch the errors below. The choice between the two keywords matters in the edge cases that follow.

1. Declaration merging: only interfaces can be reopened

Declare an interface twice in the same scope and TypeScript merges the two into one. Try the same with a type alias and you get an error:

interface Settings {
  theme: string;
}
interface Settings {
  fontSize: number;
}

// Both declarations merged into one interface
const s: Settings = { theme: 'light', fontSize: 16 };
const missing: Settings = { theme: 'dark' };

type Options = { theme: string };
type Options = { fontSize: number };
$ npx tsc --noEmit --strict --lib ES2022,DOM t02_merging.ts
t02_merging.ts(10,7): error TS2741: Property 'fontSize' is missing in type '{ theme: string; }' but required in type 'Settings'.
t02_merging.ts(12,6): error TS2300: Duplicate identifier 'Options'.
t02_merging.ts(13,6): error TS2300: Duplicate identifier 'Options'.

The first error proves the merge happened: Settings now needs fontSize as well. The main real-world use is adding to types you don't own, such as the global Window:

declare global {
  interface Window {
    appVersion: string;
  }
}

window.appVersion = '1.4.2';

That compiled cleanly. Library type definitions use the same trick to let you extend things like Express's Request. The handbook page on declaration merging covers the full rules.

The accidental merge trap

Merging also has a downside. In a file with no import or export, your declarations are global, and with the DOM types loaded, Comment already exists. It's the DOM's comment node. Declare your own Comment interface and it silently merges with that one:

// No import/export: this file is a global script, so Comment merges with the DOM's Comment
interface Comment {
  id: number;
  text: string;
}

const c: Comment = { id: 1, text: 'Nice post' };
t10_global_clash.ts(7,7): error TS2740: Type '{ id: number; text: string; }' is missing the following properties from type 'Comment': data, length, ownerDocument, appendData, and 59 more.

The error points at the object, not at the declaration that caused it, which makes it confusing. A type alias with the same name fails loudly where it's declared:

t10b_global_clash_type.ts(2,6): error TS2300: Duplicate identifier 'Comment'.

The fix for both is to make the file a module with an import or export, or to pick a less generic name. If modules and import errors are new to you, my post on Cannot use import statement outside a module explains how Node decides what counts as a module.

2. extends vs intersection: how conflicts are reported

You can build on an interface with extends, or on a type with &. They agree until two members clash:

interface Animal {
  id: string;
  name: string;
}

// interface extends: the conflict is reported right here
interface Dog extends Animal {
  id: number;
}

// type intersection: no error here, id silently becomes never
type Cat = Animal & { id: number };

const cat: Cat = { id: 1, name: 'Tom' };
$ npx tsc --noEmit --strict --lib ES2022,DOM t04_extends_conflict.ts
t04_extends_conflict.ts(7,11): error TS2430: Interface 'Dog' incorrectly extends interface 'Animal'.
  Types of property 'id' are incompatible.
    Type 'number' is not assignable to type 'string'.
t04_extends_conflict.ts(14,20): error TS2322: Type 'number' is not assignable to type 'never'.

With extends, the error lands on the declaration and explains the clash. With &, the declaration is accepted, id becomes string & number, which is never, and you only find out later when no value fits. That's a strong reason to prefer extends when you build object types from other object types. Catching shape mistakes at compile time is the point of TypeScript. In plain JavaScript they surface at runtime, as errors like TypeError: x is not a function.

3. Unions, tuples, mapped and conditional types need type

An interface can only describe an object shape. Everything else needs a type alias:

type ID = string | number;                         // union
type Point = [x: number, y: number];               // tuple
type Status = 'idle' | 'loading' | 'done';         // literal union
type Flags<T> = { [K in keyof T]: boolean };       // mapped type
type ElementOf<T> = T extends (infer U)[] ? U : T; // conditional type
type EventName = `on${Capitalize<'click' | 'focus'>}`; // template literal
$ node t05_type_only.ts
42 [ 10, 20 ] done { dark: true, beta: false } hello onClick

All six compiled and ran. Mapped types like Flags are explained in the handbook chapter on mapped types. An interface also can't extend a union, because a union isn't one object shape:

interface Success { ok: true; data: string }
interface Failure { ok: false; error: string }

type Result = Success | Failure;

// An interface cannot be a union, and cannot extend one
interface Result2 extends Result {}
t06_interface_union.ts(7,27): error TS2312: An interface can only extend an object type or intersection of object types with statically known members.

The usual pattern is the one above: an interface for each variant, and a type alias for the union of them.

4. implements works with both, with one catch

A class can implement an interface or an object type alias, and even both at once:

interface Greeter {
  greet(name: string): string;
}
type Logger = {
  log(message: string): void;
};

// A class can implement both an interface and an object type alias
class Service implements Greeter, Logger {
  greet(name: string) { return `Hello, ${name}`; }
  log(message: string) { console.log(message); }
}

type Shape = { kind: 'circle'; r: number } | { kind: 'square'; size: number };

class Circle implements Shape {
  kind = 'circle' as const;
  r = 1;
}
t07_implements.ts(16,25): error TS2422: A class can only implement an object type or intersection of object types with statically known members.

Service was fine. Circle failed because Shape is a union, and a class must match one known shape. The handbook chapter on classes notes another point: implements only checks the class. It doesn't change the types inside it.

5. You can mix them freely

The two forms work together. An interface can extend an object type alias, and a type alias can intersect an interface:

type Timestamps = { createdAt: Date; updatedAt: Date };

interface Post extends Timestamps {
  title: string;
}

type Draft = Post & { published: false };

That compiled cleanly and ran under Node. You don't have to choose one keyword for a whole codebase.

6. Implicit index signatures: a surprising difference

This one catches people out. A type alias for an object literal type can be passed where a Record<string, string> is expected. An interface with exactly the same members can't:

interface HeadersI {
  accept: string;
}
type HeadersT = {
  accept: string;
};

function send(headers: Record<string, string>) {
  console.log(headers);
}

const h1: HeadersT = { accept: 'text/html' };
const h2: HeadersI = { accept: 'text/html' };

send(h1); // OK: type aliases get an implicit index signature
send(h2); // Error: interfaces do not
t09_index_signature.ts(16,6): error TS2345: Argument of type 'HeadersI' is not assignable to parameter of type 'Record<string, string>'.
  Index signature for type 'string' is missing in type 'HeadersI'.

The reason is merging. Because an interface can be reopened later with more members, TypeScript won't assume every member is a string. If you hit this, use a type alias, add an index signature to the interface, or type the parameter more precisely. Record is one of the built-in utility types.

7. Error messages: interfaces keep their names

Interface names show up consistently in errors and editor tooltips. Type aliases built from intersections can expand into their full structure once they're combined with something else:

interface Base { id: number; name: string; secret: string }

interface PublicUserI extends Omit<Base, 'secret'> { avatar: string }
type PublicUserT = Omit<Base, 'secret'> & { avatar: string };

declare const i: PublicUserI;
declare const t: PublicUserT;

function withRole<T>(user: T) {
  return { ...user, role: 'admin' as const };
}

const r1: number = withRole(i);
const r2: number = withRole(t);
t13_error_display.ts(13,7): error TS2322: Type 'PublicUserI & { role: "admin"; }' is not assignable to type 'number'.
t13_error_display.ts(14,7): error TS2322: Type 'Omit<Base, "secret"> & { avatar: string; } & { role: "admin"; }' is not assignable to type 'number'.

The two types describe the same values, but the second message makes you read Omit<Base, "secret"> and work out what it means. In a large codebase, that adds up. For simple cases TypeScript keeps alias names too, so this mainly matters for intersections.

8. Performance: prefer interfaces for composition

The TypeScript team's Performance wiki page has a section called "Preferring Interfaces Over Intersections". It says interfaces create a single flat object type that detects property conflicts, that type relationships between interfaces are cached while intersection types as a whole are not, and that every part of a target intersection is checked before the flattened type. Its advice: when you compose types, extend them with an interface instead of intersecting them in a type alias.

I didn't benchmark this myself. With ten-line examples, the difference is too small to measure. It matters in big projects with deep type hierarchies, where type-checking time and editor responsiveness add up.

Which should you use?

  • Object shapes, props, API responses and class contracts: interface. You get clear conflict errors, consistent names in errors and the faster path for composition.
  • Unions, tuples, primitives, function types and anything computed: type. There's no alternative.
  • Adding to a library or global type: interface, because only interfaces merge.
  • Values passed as Record<string, ...>: type, or add an index signature.
  • Combining shapes: interface ... extends over & where you can.

Whatever you pick, be consistent within a project, and let a linter enforce it if your team argues about it. Plain JavaScript basics still apply underneath, so it's worth knowing the runtime side too, such as my guide to var vs let vs const.

How I tested these examples

Each snippet is its own file, compiled with tsc --noEmit --strict --lib ES2022,DOM on TypeScript 7.0.2 and 5.9.3, with Node.js 24.22.1 running the clean files. One TypeScript 7 change caught me out: with a tsconfig.json in the folder, passing a file name on the command line is now an error:

$ npx tsc t01_same_shape.ts
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

Either run plain tsc to use the config, or add --ignoreConfig as the message says. My free tools page has a few small browser-based helpers if you need to tidy JSON or test data while you work.

Frequently asked questions

What is the main difference between interface and type in TypeScript?

Interfaces can be declared more than once and merge, and they can only describe object shapes. Type aliases can't be reopened, but they can name any type, including unions, tuples, primitives, mapped and conditional types.

Should I use interface or type for React props?

Either works. Many teams use interface for props because errors show the name clearly and extends reports conflicts. Use a type alias when the props are a union, for example a component that accepts one of two different prop shapes.

Can a type alias extend an interface?

Not with the extends keyword, but it can intersect one: type Draft = Post & { published: false }. An interface can also extend an object type alias with extends, as long as the alias isn't a union.

Why does TypeScript say Duplicate identifier for my type?

Type aliases can't be declared twice in the same scope. You may have two type declarations with the same name, or, in a file with no import or export, a name that clashes with a global such as the DOM's Comment. Rename it or make the file a module.

Are interfaces faster than types?

For composing object types, the TypeScript team's Performance wiki recommends interfaces with extends over intersections, because interface relationships are cached and conflicts are flattened. For a simple object shape, there's no practical difference.

Can a class implement a type alias?

Yes, if the alias describes an object type or an intersection of object types. A class can't implement a union type alias. TypeScript reports error TS2422 because the class has to match one known shape.

Why can't I pass an interface to a Record<string, string> parameter?

Interfaces don't get an implicit index signature, because they could be merged with more members later. A type alias with the same members does. Use a type alias, add an index signature, or change the parameter type.

Last checked: 5 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.

Share X LinkedIn WhatsApp Facebook
Was this helpful?

0%