| |

TypeScript 7 ๐Ÿ”ท Type Aliases and Interfaces

TypeScript gives you two ways to name a type: type aliases (type) and interfaces (interface). They overlap heavily โ€” you can describe almost anything with either โ€” but they’re not interchangeable. Each has features the other lacks, and the choice between them has real consequences for declaration merging, error messages, and how you structure a codebase. Knowing when to use which is one of those small decisions that shapes everything downstream.

Key point: interface declares a named object shape that can be extended, implemented, and merged. type declares an alias for any type expression โ€” objects, unions, tuples, primitives, functions, you name it. If you’re describing an object, both work. If you’re describing anything else, or composing with unions and intersections, type is the only option. The community has largely settled on a default, but the reasons matter more than the rule.


Type aliases โ€” type

A type alias gives a name to any type.

type ID = string | number;
type Point = { x: number; y: number };
type Handler = (event: Event) => void;
type Pair = [string, number];
type Status = 'loading' | 'ready' | 'error';
type Maybe<T> = T | null | undefined;

Every one of those is a type alias. ID aliases a union, Point an object, Handler a function, Pair a tuple, Status a literal union, Maybe<T> a generic.

What a type alias can describe:

  • Primitives โ€” type Name = string
  • Unions โ€” type ID = string | number
  • Intersections โ€” type Entity = Base & Timestamps
  • Tuples โ€” type Point = [number, number]
  • Functions โ€” type Fn = (x: number) => string
  • Literal unions โ€” type Status = 'a' | 'b'
  • Object shapes โ€” type User = { name: string }
  • Mapped types โ€” type Optional<T> = { [K in keyof T]?: T[K] }
  • Conditional types โ€” type IsString<T> = T extends string ? true : false
  • Generic types โ€” type Result<T> = { ok: true; value: T } | { ok: false; error: string }

That breadth is the point. A type alias can name any type expression, no matter how complex.

Using a type alias:

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

const alice: User = { id: 1, name: 'Alice' };

function greet(user: User): string {
  return `Hello, ${user.name}`;
}

The syntax is identical to using an interface. The difference is what you can put on the right-hand side.

A type alias is not a new type โ€” it’s an alias. User and { id: number; name: string } are the same type. You can assign between them freely.

type A = { x: number };
type B = { x: number };

const a: A = { x: 1 };
const b: B = a;                   // โœ… structurally identical

That’s structural typing โ€” the alias is a name for the shape, not a distinct nominal type.

Why “type alias” and not “type definition”: Because that’s what it is โ€” a name for something that already exists. The underlying type isn’t new; the name is. That’s different from classes and enums, which create runtime values. A type alias exists only in the type system.


Interfaces โ€” interface

An interface declares a named object shape.

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

interface Point {
  x: number;
  y: number;
}

interface Handler {
  (event: Event): void;
}

The syntax uses interface Name { ... }. The body lists members โ€” properties, methods, index signatures, call signatures, construct signatures.

What an interface can describe:

  • Object shapes
  • Function shapes (with a call signature)
  • Constructor shapes (with a construct signature)
  • Classes (via implements)
  • Index signatures โ€” [key: string]: number
  • Callable objects โ€” properties plus a call signature
  • Extension of other interfaces and classes
  • Declaration merging (adding to an existing interface from another file)

What an interface cannot describe:

  • Union types
  • Intersection types as the whole declaration
  • Tuples
  • Primitives
  • Conditional types
  • Mapped types

For any of those, you need type.

Using an interface:

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

const alice: User = { id: 1, name: 'Alice' };

Identical usage to a type alias. The difference is in what you can define.

Why interfaces exist at all: They predate type in TypeScript’s history, and they map cleanly to how object-oriented programmers think โ€” a named shape that can be implemented by classes and extended by other interfaces. They also support declaration merging, which type doesn’t. That single feature is why libraries and ambient declarations use interfaces heavily.


The overlap โ€” when either works

For object shapes, both syntaxes work identically.

// Both describe the same shape
type UserType = {
  id: number;
  name: string;
};

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

const a: UserType = { id: 1, name: 'Alice' };
const b: UserInterface = { id: 1, name: 'Alice' };

The two types are structurally identical. You can assign between them.

const c: UserInterface = a;       // โœ…
const d: UserType = b;            // โœ…

Extension also works in both:

// Interface extends
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// Type alias with intersection
type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

Both produce a type with name and breed. Interface extends and type & are equivalent for object composition.

The practical difference shows up in two places:

  1. Declaration merging โ€” interfaces can be merged; type aliases can’t
  2. Error messages โ€” interfaces show their name; type aliases sometimes expand inline

And one more: implementing in a class.

interface Serializable {
  serialize(): string;
}

class User implements Serializable {
  serialize(): string { return JSON.stringify(this); }
}

A class can implement an interface. A class can’t implement a type alias that isn’t an object type โ€” and even for object-shaped aliases, implements with a union alias fails.

Why the overlap is fine: TypeScript’s team kept both because they serve overlapping but distinct use cases. When they overlap โ€” describing a plain object โ€” either works, and consistency matters more than which one. When they don’t overlap โ€” union, tuple, primitive, conditional โ€” the choice is forced.


Declaration merging

This is the single feature interfaces have that type aliases don’t. An interface can be declared multiple times, and TypeScript merges the declarations.

interface User {
  id: number;
}

interface User {
  name: string;
}

// Merged:
// interface User {
//   id: number;
//   name: string;
// }

const alice: User = { id: 1, name: 'Alice' };

Both id and name are required. TypeScript merges the two declarations into one.

What merges:

  • Interfaces with the same name in the same scope
  • Namespaces
  • Enums (partially)
  • A namespace and a class/function/enum with the same name

What doesn’t merge:

  • Type aliases โ€” declaring the same type name twice is an error
  • Classes โ€” duplicate class declarations are an error

Where merging is useful:

Augmenting third-party libraries. A library declares interface Window { ... } and you add a property:

// In your code
declare global {
  interface Window {
    myApp: {
      version: string;
    };
  }
}

window.myApp.version;             // โœ…

Ambient declarations. When .d.ts files from different sources describe the same interface, they merge.

Plugin architecture. A library exposes an interface that consumers extend with their own fields.

Why merging matters: Libraries can’t know what fields a consumer will add. Declaration merging lets the consumer extend the interface without modifying the library. Without it, every extension would need a subclass or a wrapper type โ€” and neither composes as cleanly.

Why type aliases don’t merge: A type alias is an alias โ€” type A = X โ€” so type A = Y would be two names for the same identifier, which is a conflict. An interface declares a shape that can grow. The two models are fundamentally different, and merging is a consequence of that.


Extending โ€” extends vs &

Both interfaces and type aliases support extension, but the mechanism differs.

Interface extends:

interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

The child inherits the parent’s members. Interface extends accepts:

  • Another interface
  • A class (rare)
  • Multiple parents: interface Dog extends Animal, Pet
  • Generics: interface Box<T> extends Container<T>

Type alias intersection:

type Animal = { name: string };
type Dog = Animal & { breed: string };

The child is an intersection. It accepts:

  • Another type alias
  • An interface
  • Multiple types: A & B & C
  • Generics: type Box<T> = Container<T> & { value: T }

The subtle differences:

Conflict detection. Interfaces catch conflicts at the extends site:

interface A { value: string; }
interface B { value: number; }
interface C extends A, B { }      // โŒ error: property value has conflicting types

Type aliases resolve conflicts via intersection โ€” the property becomes never:

type A = { value: string };
type B = { value: number };
type C = A & B;
// C.value is string & number โ†’ never
// No error at the declaration, but C is unusable

When you want conflicts to be errors, interface extends is safer. When you’re combining types that might overlap and want them to merge (or you know they don’t conflict), intersection works.

Performance. interface extends produces types TypeScript can cache by name. Deeply nested intersections can slow the compiler. For large codebases, interfaces extend faster.

Both produce the same shape for non-conflicting cases. The choice between them is about error handling and compiler behavior.

Why interface extends errors on conflict: An interface declares one shape with a required set of members. If two parents require different types for the same member, there’s no valid shape โ€” so it’s an error. An intersection declares “both types,” and where they conflict, the intersection is empty for that property โ€” never. Both are consistent; they’re just different trade-offs.


Interface vs type alias โ€” full comparison

FeatureInterfaceType Alias
Object shapeโœ…โœ…
Function shapeโœ… (call signature)โœ…
Union typeโŒโœ…
Intersection at top levelโŒโœ…
TupleโŒโœ…
Primitive aliasโŒโœ…
Literal unionโŒโœ…
Mapped typeโŒโœ…
Conditional typeโŒโœ…
Template literal typeโŒโœ…
Extends anotherโœ… (extends)โœ… (&)
Implements (by class)โœ…โš ๏ธ object aliases only
Declaration mergingโœ…โŒ
Genericโœ…โœ…
Index signatureโœ…โœ…
Call signatureโœ…โœ…
Construct signatureโœ…โœ…
Recursiveโœ…โœ…
Error conflicts at declarationโœ…โŒ
Cached by nameโœ…โš ๏ธ sometimes
Duplicate nameโœ… mergesโŒ error

When the answer is obvious:

  • Need a union โ†’ type
  • Need a tuple โ†’ type
  • Need a primitive alias โ†’ type
  • Need a conditional or mapped type โ†’ type
  • Need declaration merging โ†’ interface
  • Need a class to implement โ†’ interface

When either works โ€” plain object shapes:

The community’s default is: use interface for object shapes, type for everything else. This is not a law; it’s a convention that most codebases and style guides follow.

Why that default: Interfaces are designed for the object case. They extend cleanly, error on conflicts, cache well, and support merging. Type aliases are the general-purpose tool โ€” they can do objects but exist to cover the cases interfaces can’t. Using each for what it’s designed for keeps code consistent and compiler-friendly.


Why the convention matters

The TypeScript team’s own guidance and the Google TypeScript Style Guide both recommend preferring interfaces over type aliases for object types. The reasons:

Better error messages. When a type mismatch happens, the interface name appears in the error rather than the expanded shape.

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

const u1: User = { id: 'x', name: 'Alice' };
// Error: Type 'string' is not assignable to type 'number'.
//   Property 'id' of type 'User'

const u2: UserT = { id: 'x', name: 'Alice' };
// Error: Type 'string' is not assignable to type 'number'.
//   Property 'id' of type '{ id: number; name: string }'

The interface version names the type; the alias version expands it. Over long sessions of debugging, that difference adds up.

Faster compilation. Interfaces are cached by name. Type aliases may be re-evaluated in some scenarios. For very large codebases, the difference is measurable.

Declaration merging. Interfaces can be extended by consumers without touching the original. Libraries rely on this.

Extends errors earlier. Conflicts are caught where the extension happens, not where the type is used.

What to do: Use interface for object shapes. Use type for unions, tuples, primitives, functions when you want a named alias, and anything more complex. Both are fine for object shapes, but the convention has concrete benefits.

Why not always use type: Some codebases standardize on type for everything and do fine. It works. But you lose declaration merging, error messages are worse, and extends doesn’t catch conflicts. For a small codebase that never merges, those costs are small. For a large one, they compound. Following the convention costs nothing and avoids the issues.


Advanced uses

Interfaces with call signatures โ€” callable objects:

interface Logger {
  (message: string): void;
  level: 'info' | 'warn' | 'error';
}

declare const log: Logger;
log('hello');                     // callable
log.level;                        // has a property

An interface can describe something callable and with properties.

Interfaces with construct signatures:

interface UserConstructor {
  new (name: string): { name: string };
}

function createUser(Ctor: UserConstructor, name: string) {
  return new Ctor(name);
}

Index signatures:

interface Dictionary {
  [key: string]: number;
}

interface WithId {
  id: string;
  [key: string]: string;
}

Recursive interfaces:

interface TreeNode {
  value: number;
  children: TreeNode[];
}

Generic interfaces:

interface Box<T> {
  value: T;
}

interface Pair<A, B> {
  first: A;
  second: B;
}

All of these work with type aliases too, except declaration merging. The choice between them here is stylistic.

Why interfaces support call and construct signatures: They model JavaScript’s flexible reality. Functions are objects; constructors are functions; anything can have properties. Interfaces let you describe all of that declaratively. Type aliases can express the same things with different syntax.


A full example

A small domain model that uses both interfaces and type aliases, choosing based on the rule.

// ============================================
// INTERFACES โ€” object shapes
// ============================================

interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface SoftDelete {
  deletedAt: Date | null;
}

interface User extends Timestamps, SoftDelete {
  id: string;
  name: string;
  email: string;
  role: Role;
}

// ============================================
// TYPE ALIASES โ€” unions, primitives, functions
// ============================================

type Role = 'admin' | 'user' | 'guest';

type UserId = string;

type UserMap = Record<UserId, User>;

type UserHandler = (user: User) => void;

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

// ============================================
// USING THEM TOGETHER
// ============================================

function findUser(
  users: UserMap,
  id: UserId
): Result<User> {
  const user = users[id];
  if (!user) {
    return { ok: false, error: `User ${id} not found` };
  }
  return { ok: true, value: user };
}

function process(users: UserMap, handler: UserHandler): void {
  for (const user of Object.values(users)) {
    if (user.deletedAt === null) {
      handler(user);
    }
  }
}

// Usage
const alice: User = {
  id: 'u-1',
  name: 'Alice',
  email: 'alice@example.com',
  role: 'admin',
  createdAt: new Date(),
  updatedAt: new Date(),
  deletedAt: null
};

const users: UserMap = { [alice.id]: alice };

const result = findUser(users, 'u-1');
if (result.ok) {
  console.log('Found:', result.value.name);
} else {
  console.log('Error:', result.error);
}

What’s an interface here: Timestamps, SoftDelete, User โ€” object shapes with extensions.

What’s a type alias here: Role, UserId, UserMap, UserHandler, Result<T> โ€” a literal union, a primitive alias, a Record utility, a function signature, and a generic discriminated union.

The choice follows the rule: interfaces for object shapes, type aliases for everything else.

Why this combination: Every type does the job it’s best at. User extends Timestamps and SoftDelete via interface inheritance โ€” clean and cached. Result<T> is a discriminated union โ€” impossible with an interface. Each type maps to its purpose, and the codebase is more readable because of it.


Complete Example Session

# ============================================
# PART 1: INTERFACE BASICS
# ============================================

cat > interfaces.ts << 'EOF'
interface User {
  id: number;
  name: string;
}

const alice: User = { id: 1, name: 'Alice' };

// Declaration merging
interface User {
  email?: string;
}

const bob: User = { id: 2, name: 'Bob', email: 'bob@example.com' };

console.log(alice, bob);
EOF

npx tsc --noEmit interfaces.ts
# (no errors)

# ============================================
# PART 2: TYPE ALIAS BASICS
# ============================================

cat > aliases.ts << 'EOF'
type ID = string | number;
type Point = { x: number; y: number };
type Handler = (event: Event) => void;
type Pair = [string, number];
type Status = 'loading' | 'ready' | 'error';

const id: ID = 'abc';
const p: Point = { x: 1, y: 2 };
const s: Status = 'ready';

console.log(id, p, s);
EOF

npx tsc --noEmit aliases.ts
# (no errors)

# ============================================
# PART 3: DECLARATION MERGING
# ============================================

cat > merge.ts << 'EOF'
interface Config {
  apiUrl: string;
}

interface Config {
  timeout: number;
}

const cfg: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

// The following would fail โ€” type aliases don't merge
// type T = { a: string };
// type T = { b: number };  // โŒ duplicate identifier

console.log(cfg);
EOF

npx tsc --noEmit merge.ts
# (no errors)

# ============================================
# PART 4: INTERFACE EXTENDS VS INTERSECTION
# ============================================

cat > extend.ts << 'EOF'
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

const d1: Dog = { name: 'Rex', breed: 'Lab' };
const d2: DogT = { name: 'Rex', breed: 'Lab' };

// Interface catches conflicts
interface A { value: string; }
interface B { value: number; }
// interface C extends A, B { }  // โŒ conflicting types

// Type alias produces never
type AT = { value: string };
type BT = { value: number };
type CT = AT & BT;
// CT.value is never

console.log(d1, d2);
EOF

npx tsc --noEmit extend.ts
# (no errors)

# ============================================
# PART 5: FULL DOMAIN EXAMPLE
# ============================================

cat > domain.ts << 'EOF'
interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface User extends Timestamps {
  id: string;
  name: string;
  role: Role;
}

type Role = 'admin' | 'user' | 'guest';

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function makeUser(name: string): Result<User> {
  if (!name) return { ok: false, error: 'Name required' };
  return {
    ok: true,
    value: {
      id: 'u-1',
      name,
      role: 'user',
      createdAt: new Date(),
      updatedAt: new Date()
    }
  };
}

const r = makeUser('Alice');
console.log(r.ok ? r.value.name : r.error);
EOF

npx tsc --noEmit domain.ts
# (no errors)

# ============================================
# PART 6: COMPILE AND RUN
# ============================================

npx tsc interfaces.ts aliases.ts merge.ts extend.ts domain.ts
node interfaces.js
# [ { id: 1, name: 'Alice' } { id: 2, name: 'Bob', email: 'bob@example.com' } ]

node aliases.js
# [ abc { x: 1, y: 2 } ready ]

node domain.js
# [ Alice ]

Quick Reference

Syntax

FormPurpose
interface Name { ... }Declare an object shape
type Name = ...Alias any type expression

What Each Can Describe

FeatureInterfaceType Alias
Object shapeโœ…โœ…
UnionโŒโœ…
IntersectionโŒโœ…
TupleโŒโœ…
Primitive aliasโŒโœ…
Literal unionโŒโœ…
Function signatureโœ…โœ…
Mapped typeโŒโœ…
Conditional typeโŒโœ…
Template literal typeโŒโœ…
Index signatureโœ…โœ…
Call signatureโœ…โœ…
Construct signatureโœ…โœ…

Extension

InterfaceType Alias
Syntaxextends&
Multiple parentsextends A, BA & B
Conflict detectionโŒ At declarationโŒ Silent โ†’ never
Cached by nameโœ…โš ๏ธ Sometimes

Declaration Merging

InterfaceType Alias
Duplicate nameโœ… MergesโŒ Error
Use caseAugment librariesNot possible

Class Implementation

InterfaceType Alias
class X implements Nameโœ…โœ… (object aliases only)
Union aliasโŒโŒ

When to Use Which

Use caseRecommendation
Object shapeinterface
Uniontype
Intersectiontype
Tupletype
Primitive aliastype
Function typetype (or interface with call signature)
Literal uniontype
Declaration merginginterface
Class implementsinterface
Mapped/conditional typetype

Common Utility Type Aliases

AliasMeaning
type ID = string | numberID type
type Status = 'a' | 'b'Literal union
type Handler = (x: T) => voidFunction type
type Maybe<T> = T | null | undefinedNullable
type Result<T> = { ok: true; value: T } | { ok: false; error: string }Result type

Best Practices

โœ… Do This:

// Use interfaces for object shapes
interface User { id: number; name: string; }             // โœ…

// Use type aliases for unions, tuples, primitives
type Status = 'loading' | 'ready';                        // โœ…
type Pair = [string, number];                             // โœ…
type ID = string | number;                                // โœ…

// Use interfaces to extend cleanly
interface Admin extends User { permissions: string[]; }   // โœ…

// Use interfaces for class contracts
interface Serializable { serialize(): string; }           // โœ…

// Use declaration merging to augment
declare global {
  interface Window { myApp: App; }                        // โœ…
}

// Use generic type aliases for complex patterns
type Result<T> = { ok: true; value: T } | { ok: false; error: string };  // โœ…

// Prefer interface extends for conflict detection
interface C extends A, B { }                              // โœ…

โŒ Don’t Do This:

// Don't use type alias for object where interface fits
type User = { id: number; name: string };                 // โš ๏ธ  interface preferred

// Don't try to merge type aliases
type A = { x: 1 };
type A = { y: 2 };  // โŒ duplicate identifier                // โŒ

// Don't intersect conflicting object types
type C = { x: string } & { x: number };                   // โš ๏ธ  x is never

// Don't use interface for unions
interface X = 'a' | 'b';  // โŒ syntax error                  // โŒ

// Don't use type alias for tuples where object is clearer
type User = [string, string, number];                     // โš ๏ธ  use interface

// Don't ignore the convention inconsistently
type A = { x: 1 };
interface B { y: 2 }                                      // โš ๏ธ  pick one style

Common Pitfalls

PitfallProblemSolution
Trying to merge type aliasesDuplicate identifier errorUse interfaces
Conflicting interface extendsCompile errorResolve conflict
Conflicting intersectionProperty becomes neverAlign types
Using interface for unionsSyntax errorUse type
Expecting type aliases to be nominalStructural โ€” they’re aliasesUnderstand structural typing
Missing declare globalAugmentation doesn’t applyWrap in declare global
Naming a class and interface the sameMerging may surpriseUnderstand class/interface merge
Using type everywhereLose merging, worse errorsFollow convention
Extending a class in interfaceConfusing side effectsUnderstand the rules

Real-World Examples

1. Interface for a user

interface User {
  id: number;
  name: string;
  email: string;
}

2. Type alias for a union

type Status = 'idle' | 'loading' | 'ready' | 'error';

3. Type alias for a tuple

type Point = [x: number, y: number];

4. Type alias for a primitive

type UserId = string;

5. Type alias for a function

type Handler = (event: Event) => void;

6. Interface extension

interface Admin extends User {
  permissions: string[];
}

7. Type alias with intersection

type Admin = User & { permissions: string[] };

8. Declaration merging

interface User { id: number; }
interface User { name: string; }
// User has both id and name

9. Augmenting Window

declare global {
  interface Window {
    analytics: Analytics;
  }
}

10. Class implements interface

interface Serializable {
  serialize(): string;
}

class User implements Serializable {
  serialize(): string { return JSON.stringify(this); }
}

11. Generic interface

interface Box<T> {
  value: T;
}

12. Generic type alias

type Maybe<T> = T | null;

13. Discriminated union via type alias

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

14. Recursive interface

interface TreeNode {
  value: number;
  children: TreeNode[];
}

15. Index signature

interface Dictionary {
  [key: string]: number;
}

16. Callable interface

interface Logger {
  (message: string): void;
  level: 'info' | 'error';
}

17. Construct signature

interface Constructor {
  new (name: string): object;
}

18. Interface extending a class

class Point {
  x = 0;
  y = 0;
}
interface Point3D extends Point {
  z: number;
}

19. Literal union type alias

type Direction = 'north' | 'south' | 'east' | 'west';

20. Combined pattern

interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface Entity extends Timestamps {
  id: string;
}

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

Visual: Interface vs Type Alias

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface                                   โ”‚
โ”‚                                              โ”‚
โ”‚  interface User {                            โ”‚
โ”‚    id: number;                               โ”‚
โ”‚    name: string;                             โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข Object shapes                             โ”‚
โ”‚  โ€ข Extends other interfaces/classes          โ”‚
โ”‚  โ€ข Implements in classes                     โ”‚
โ”‚  โ€ข Declaration merging                       โ”‚
โ”‚  โ€ข Cached by name                            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type alias                                  โ”‚
โ”‚                                              โ”‚
โ”‚  type User = {                               โ”‚
โ”‚    id: number;                               โ”‚
โ”‚    name: string;                             โ”‚
โ”‚  };                                          โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข Any type expression                       โ”‚
โ”‚  โ€ข Unions, tuples, primitives                โ”‚
โ”‚  โ€ข Mapped, conditional types                 โ”‚
โ”‚  โ€ข No merging                                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: The Venn Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                                              โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”‚
โ”‚      โ”‚                  โ”‚                    โ”‚
โ”‚      โ”‚   Both can do:   โ”‚                    โ”‚
โ”‚      โ”‚                  โ”‚                    โ”‚
โ”‚      โ”‚   โ€ข Object shape โ”‚                    โ”‚
โ”‚      โ”‚   โ€ข Function sig โ”‚                    โ”‚
โ”‚      โ”‚   โ€ข Generics     โ”‚                    โ”‚
โ”‚      โ”‚   โ€ข Extend       โ”‚                    โ”‚
โ”‚      โ”‚   โ€ข Recursive    โ”‚                    โ”‚
โ”‚      โ”‚                  โ”‚                    โ”‚
โ”‚      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ”‚
โ”‚                                              โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€   โ”‚
โ”‚                                              โ”‚
โ”‚  interface only:      type only:             โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข Merging            โ€ข Unions               โ”‚
โ”‚  โ€ข Extends A, B       โ€ข Intersections        โ”‚
โ”‚  โ€ข Class implements   โ€ข Tuples               โ”‚
โ”‚  โ€ข Cached name        โ€ข Primitives           โ”‚
โ”‚                       โ€ข Literal unions       โ”‚
โ”‚                       โ€ข Mapped types         โ”‚
โ”‚                       โ€ข Conditional types    โ”‚
โ”‚                       โ€ข Template literals    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Declaration Merging

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  File A:                                     โ”‚
โ”‚                                              โ”‚
โ”‚  interface User {                            โ”‚
โ”‚    id: number;                               โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  File B:                                     โ”‚
โ”‚                                              โ”‚
โ”‚  interface User {                            โ”‚
โ”‚    name: string;                             โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  merged by the compiler
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Effective User:                             โ”‚
โ”‚                                              โ”‚
โ”‚  interface User {                            โ”‚
โ”‚    id: number;                               โ”‚
โ”‚    name: string;                             โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Extends vs Intersection

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface extends                           โ”‚
โ”‚                                              โ”‚
โ”‚  interface A { x: string; }                  โ”‚
โ”‚  interface B { x: number; }                  โ”‚
โ”‚  interface C extends A, B { }                โ”‚
โ”‚                                              โ”‚
โ”‚  โŒ Error at declaration                      โ”‚
โ”‚  (conflicting types for x)                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type & intersection                         โ”‚
โ”‚                                              โ”‚
โ”‚  type A = { x: string };                     โ”‚
โ”‚  type B = { x: number };                     โ”‚
โ”‚  type C = A & B;                             โ”‚
โ”‚                                              โ”‚
โ”‚  โš ๏ธ  No error at declaration                  โ”‚
โ”‚  C.x becomes never โ€” unusable                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: When to Use Which

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Plain object?                               โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ YES โ”€โ”€โ–บ interface                  โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ NO                                  โ”‚
โ”‚            โ”‚                                 โ”‚
โ”‚            โ”œโ”€โ”€ Union? โ”€โ”€โ–บ type                โ”‚
โ”‚            โ”œโ”€โ”€ Tuple? โ”€โ”€โ–บ type                โ”‚
โ”‚            โ”œโ”€โ”€ Primitive? โ”€โ”€โ–บ type            โ”‚
โ”‚            โ”œโ”€โ”€ Mapped/conditional? โ”€โ”€โ–บ type   โ”‚
โ”‚            โ””โ”€โ”€ Function? โ”€โ”€โ–บ type             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Error Message Quality

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  With interface:                             โ”‚โ”‚                                              โ”‚
โ”‚  interface User { id: number }               โ”‚
โ”‚  const u: User = { id: 'x' };                โ”‚
โ”‚                                              โ”‚
โ”‚  Error: Type 'string' is not assignable      โ”‚
โ”‚  to type 'number'.                           โ”‚
โ”‚  Property 'id' of type 'User'.               โ”‚
โ”‚                          โ†‘                   โ”‚
โ”‚                     name shown               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  With type alias:                            โ”‚
โ”‚                                              โ”‚
โ”‚  type User = { id: number };                 โ”‚
โ”‚  const u: User = { id: 'x' };                โ”‚
โ”‚                                              โ”‚
โ”‚  Error: Type 'string' is not assignable      โ”‚
โ”‚  to type 'number'.                           โ”‚
โ”‚  Property 'id' of type '{ id: number }'.     โ”‚
โ”‚                          โ†‘                   โ”‚
โ”‚                     expanded                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptInterfaceType Alias
Object shapesโœ…โœ…
UnionsโŒโœ…
TuplesโŒโœ…
PrimitivesโŒโœ…
Conditional typesโŒโœ…
Mapped typesโŒโœ…
Function signaturesโœ…โœ…
Declaration mergingโœ…โŒ
Class implementsโœ…โš ๏ธ object aliases only
Cached by nameโœ…โš ๏ธ sometimes
Conflict detection on extendโœ…โŒ silent never

Key takeaways:

  • interface declares a named object shape; type aliases any type expression
  • Both can describe object shapes โ€” the overlap is real
  • Only type can describe unions, intersections, tuples, primitives, mapped types, conditional types, and template literal types
  • Only interface supports declaration merging โ€” multiple declarations with the same name merge
  • interface extends catches conflicts at the declaration; type & silently produces never
  • Classes implement interfaces directly โ€” implements Name
  • Error messages are better with interfaces โ€” the name appears instead of the expanded shape
  • Compilation is faster with interfaces โ€” they’re cached by name
  • The community convention: interface for object shapes, type for everything else
  • Neither is “better” โ€” each is designed for a specific set of use cases
  • Use type when you need a union, tuple, primitive alias, or complex type
  • Use interface when you need merging, better errors, or a class contract

Remember: Both name types. Interfaces are specialized for object shapes โ€” they extend cleanly, merge across files, and produce good error messages. Type aliases are general-purpose โ€” they name anything, including unions and tuples that interfaces can’t express. The convention โ€” interface for objects, type for the rest โ€” isn’t arbitrary; it reflects how the two features are designed. Follow it and both features do their best work.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!