Learn

/

Interface vs Type

Interface vs Type

6 patterns

When to use interface, when to use type, declaration merging, and extending vs intersecting. You'll hit this when you're unsure which to pick and whether it actually matters.

Avoid
type User = {
  name: string;
  role: string;
};

// Intersection: silently merges conflicts
type AdminUser = User & {
  role: number; // No error here!
};

// role is 'string & number' which is 'never'
// You only find out when you try to use it
const admin: AdminUser = {
  name: "Alice",
  role: "admin", // Error: never
};
type User = {
  name: string;
  role: string;
};

// Intersection: silently merges conflicts
type AdminUser = User & {
  role: number; // No error here!
};

// role is 'string & number' which is 'never'
// You only find out when you try to use it
const admin: AdminUser = {
  name: "Alice",
  role: "admin", // Error: never
};

Prefer
interface User {
  name: string;
  role: string;
}

// Extends: catches conflicts immediately
// interface AdminUser extends User {
//   role: number; // Error: not assignable
// }

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

const admin: AdminUser = {
  name: "Alice",
  role: "admin", // OK
  permissions: ["read"],
};
interface User {
  name: string;
  role: string;
}

// Extends: catches conflicts immediately
// interface AdminUser extends User {
//   role: number; // Error: not assignable
// }

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

const admin: AdminUser = {
  name: "Alice",
  role: "admin", // OK
  permissions: ["read"],
};
Why avoid

Type aliases work for object shapes and can be extended with & intersections. However, intersections silently merge conflicting properties into never instead of erroring. Types also cannot be augmented via declaration merging. For plain object shapes, interfaces are the conventional choice.

Why prefer

Interfaces are the idiomatic choice for object shapes. They support clean extends inheritance (which catches property conflicts at declaration), declaration merging for augmenting third-party types, and produce clearer error messages. Both types and interfaces can be extended, but interfaces make the intent more explicit.

TypeScript Handbook: Interfaces vs Types
Avoid
// Can't express a union with interface
// interface Result = Success | Failure; // Syntax error

// Workaround: single interface with optionals
interface Result {
  success: boolean;
  data?: string;
  error?: string;
}

// Not type-safe: can have both data and error
// Can't express a union with interface
// interface Result = Success | Failure; // Syntax error

// Workaround: single interface with optionals
interface Result {
  success: boolean;
  data?: string;
  error?: string;
}

// Not type-safe: can have both data and error

Prefer
interface Success {
  status: "ok";
  data: string;
}

interface Failure {
  status: "error";
  error: string;
}

// Union type: only type aliases can do this
type Result = Success | Failure;

// Type-safe: data and error are mutually exclusive
interface Success {
  status: "ok";
  data: string;
}

interface Failure {
  status: "error";
  error: string;
}

// Union type: only type aliases can do this
type Result = Success | Failure;

// Type-safe: data and error are mutually exclusive
Why avoid

A single interface with optional fields cannot express mutual exclusivity. Nothing prevents a value from having both data and error, or neither. A discriminated union with a status field guarantees exactly one variant at a time.

Why prefer

Union types can only be expressed with the type keyword. Interfaces cannot be combined with |. Define each variant as an interface for its object shape, then use a type alias to create the union. This gives you the best of both worlds.

TypeScript Handbook: Union types
Avoid
type Animal = {
  name: string;
  legs: number;
};

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

// Intersection issues:
// - Conflicting properties become 'never' silently
type A = { x: number };
type B = { x: string };
type C = A & B; // x is 'never' (number & string)
type Animal = {
  name: string;
  legs: number;
};

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

// Intersection issues:
// - Conflicting properties become 'never' silently
type A = { x: number };
type B = { x: string };
type C = A & B; // x is 'never' (number & string)

Prefer
interface Animal {
  name: string;
  legs: number;
}

interface Dog extends Animal {
  breed: string;
}

// Extends catches conflicts at declaration:
// interface Bad extends Animal {
//   name: number; // Error: not assignable to string
// }
interface Animal {
  name: string;
  legs: number;
}

interface Dog extends Animal {
  breed: string;
}

// Extends catches conflicts at declaration:
// interface Bad extends Animal {
//   name: number; // Error: not assignable to string
// }
Why avoid

Intersection types silently merge conflicting properties into never. number & string is never because no value can be both. This compiles without error at the type definition, but any attempt to assign a value to x fails. extends catches the conflict immediately.

Why prefer

extends catches property type conflicts at the point of declaration. If you try to override name with an incompatible type, TypeScript immediately reports an error. With &, conflicting properties silently become never, which only causes errors later when you try to use the value.

TypeScript Handbook: Extending types
Avoid
// Can't merge type aliases
type Window = {
  title: string;
};

type Window = {
  // Error: Duplicate identifier 'Window'
  appVersion: string;
};
// Can't merge type aliases
type Window = {
  title: string;
};

type Window = {
  // Error: Duplicate identifier 'Window'
  appVersion: string;
};

Prefer
// Interfaces merge automatically
interface Window {
  title: string;
}

interface Window {
  appVersion: string;
}

// Result: Window has both title and appVersion
// Useful for augmenting third-party types:
declare global {
  interface Window {
    analytics: AnalyticsLib;
  }
}
// Interfaces merge automatically
interface Window {
  title: string;
}

interface Window {
  appVersion: string;
}

// Result: Window has both title and appVersion
// Useful for augmenting third-party types:
declare global {
  interface Window {
    analytics: AnalyticsLib;
  }
}
Why avoid

Type aliases are closed after declaration. Defining the same type alias twice is a compile error. If you need to add properties to an existing type from another module (like Window or a library type), you must use an interface.

Why prefer

Interfaces with the same name in the same scope are automatically merged. This is essential for augmenting global types or extending third-party library types without modifying their source. Type aliases cannot be reopened, so they do not support this pattern.

TypeScript Handbook: Declaration merging
Avoid
interface ApiResponse {
  user: {
    id: string;
    profile: {
      name: string;
      avatar: string;
    };
  };
  posts: { id: string; title: string }[];
}

// Manually duplicating nested types
interface Profile {
  name: string;
  avatar: string;
}

interface Post {
  id: string;
  title: string;
}
interface ApiResponse {
  user: {
    id: string;
    profile: {
      name: string;
      avatar: string;
    };
  };
  posts: { id: string; title: string }[];
}

// Manually duplicating nested types
interface Profile {
  name: string;
  avatar: string;
}

interface Post {
  id: string;
  title: string;
}

Prefer
interface ApiResponse {
  user: {
    id: string;
    profile: {
      name: string;
      avatar: string;
    };
  };
  posts: { id: string; title: string }[];
}

// Extract nested types with indexed access
type Profile = ApiResponse["user"]["profile"];
// { name: string; avatar: string }

type Post = ApiResponse["posts"][number];
// { id: string; title: string }
interface ApiResponse {
  user: {
    id: string;
    profile: {
      name: string;
      avatar: string;
    };
  };
  posts: { id: string; title: string }[];
}

// Extract nested types with indexed access
type Profile = ApiResponse["user"]["profile"];
// { name: string; avatar: string }

type Post = ApiResponse["posts"][number];
// { id: string; title: string }
Why avoid

Manually writing separate interfaces for nested shapes creates multiple sources of truth. If the API response changes the profile shape, the hand-written Profile interface is silently wrong. Indexed access types keep everything derived from one source.

Why prefer

Indexed access types let you extract nested types from existing types using bracket notation. ApiResponse["user"]["profile"] drills into the structure, and [number] extracts the element type from an array. Changes to the source type automatically propagate.

TypeScript Handbook: Indexed access types
Avoid
// Using type for everything
type Props = {
  children: React.ReactNode;
  onClick: () => void;
};

type Theme = "light" | "dark";

type Merge<A, B> = Omit<A, keyof B> & B;

// No consistency, no reasoning about
// when to use type vs interface
// Using type for everything
type Props = {
  children: React.ReactNode;
  onClick: () => void;
};

type Theme = "light" | "dark";

type Merge<A, B> = Omit<A, keyof B> & B;

// No consistency, no reasoning about
// when to use type vs interface

Prefer
// Interface for object shapes and contracts
interface Props {
  children: React.ReactNode;
  onClick: () => void;
}

// Type for unions, intersections, and utilities
type Theme = "light" | "dark";

type Merge<A, B> = Omit<A, keyof B> & B;

// Rule of thumb:
// - interface: object shapes, class contracts, extendable APIs
// - type: unions, mapped types, conditional types, computed types
// Interface for object shapes and contracts
interface Props {
  children: React.ReactNode;
  onClick: () => void;
}

// Type for unions, intersections, and utilities
type Theme = "light" | "dark";

type Merge<A, B> = Omit<A, keyof B> & B;

// Rule of thumb:
// - interface: object shapes, class contracts, extendable APIs
// - type: unions, mapped types, conditional types, computed types
Why avoid

Using type for everything works, but you miss out on extends syntax, declaration merging, and clearer error messages for object shapes. Having a clear convention helps teams make consistent decisions and produces more readable code.

Why prefer

The practical rule: use interface for object shapes you might extend or that represent contracts (props, API responses, class shapes). Use type for unions, computed types, mapped types, and anything that cannot be expressed as an interface. Consistency within a codebase matters more than the choice itself.

TypeScript Handbook: Interfaces vs Types