Interface vs Type in TypeScript: Current Best Practices
interfacestypesbest-practicestypescript-fundamentalslanguage

Interface vs Type in TypeScript: Current Best Practices

TTypeScript Page Editorial
2026-06-10
9 min read

A practical guide to choosing between interface and type in TypeScript, with defaults, examples, and team-friendly best practices.

If you have spent time with TypeScript, you have probably asked some version of the same question: should this be an interface or a type? The confusing part is that both can describe object shapes, both show up in real codebases, and both are valid in many of the same places. This guide gives you a practical way to choose. Instead of treating the decision as a style debate, it explains the tradeoffs, shows where each construct is strongest, and ends with a set of defaults you can keep using as TypeScript evolves.

Overview

The short answer is simple: in modern TypeScript, interface and type overlap a lot, but they are not identical. If you only remember one rule, make it this one: use interface for object-shaped contracts that may need extension or implementation, and use type for composition, unions, primitives, mapped types, tuples, and utility-heavy type logic.

That default works well because it matches how most teams think about the language:

  • Interfaces describe shapes that feel like named contracts.
  • Type aliases are more general and work better when you are building types from other types.

Here is the important context. You can write either of these:

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

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

For a plain object shape, both are usually fine. That is exactly why the choice becomes hard. The best practice is not to search for an absolute winner. It is to choose the construct that communicates intent with the least friction for your team.

A useful way to frame the decision:

  • If the type is mainly a public shape, start with interface.
  • If the type is mainly a derived expression, start with type.
  • If both work and the code is local, pick the one that makes future edits easier.

This article focuses on that practical distinction rather than language trivia.

How to compare options

The cleanest way to compare interface vs type is to ask four questions before you write either one.

1. Is this a contract or a composition?

If you are naming a stable shape that represents a domain object, request payload, props object, service contract, or class implementation target, interface is often a better fit.

interface CreateUserRequest {
  email: string;
  password: string;
}

If you are composing several pieces, narrowing values, or expressing alternatives, type is usually clearer.

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

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

2. Will it need declaration merging or augmentation?

Interfaces support declaration merging. That means multiple declarations with the same name can combine into one shape. This can be useful in library design, framework integrations, or controlled augmentation points.

interface Window {
  appVersion: string;
}

You cannot do that with a type alias. If your code depends on augmentation, interface is the right tool.

For most app code, though, declaration merging is not a daily need. It is helpful, but it should not be the main reason to prefer interfaces everywhere.

3. Am I expressing something that interfaces cannot model directly?

Type aliases can represent many things interfaces cannot represent directly in the same form:

  • unions
  • intersections
  • primitive aliases
  • tuples
  • conditional types
  • mapped types
type ID = string;

type Point = [number, number];

type Nullable<T> = T | null;

type ReadonlyUser<T> = {
  readonly [K in keyof T]: T[K];
};

Once your types become expressive building blocks, type is usually the better choice.

4. Who will read this next?

This is easy to overlook. A junior developer reading interface Product usually assumes “this is a shape I can depend on.” A reader seeing type Product = ... often expects some amount of type-level composition, even if the result is still an object.

That expectation matters. Good TypeScript style is partly about reducing surprise. If an object is simple and important, an interface can make the code feel more direct. If the type is assembled from utility types and transformations, a type alias better matches the mental model.

Feature-by-feature breakdown

Here is where the differences matter in day-to-day development.

Object shape declarations

Both constructs work for object shapes.

interface Account {
  id: string;
  email: string;
}

type Account = {
  id: string;
  email: string;
};

In this case, the main difference is intent. If the shape is a durable contract, use interface. If it is local or likely to be combined with other types, use type.

Extension and composition

Interfaces use extends. Types often use intersections with &.

interface BaseUser {
  id: string;
}

interface AdminUser extends BaseUser {
  permissions: string[];
}
type BaseUser = {
  id: string;
};

type AdminUser = BaseUser & {
  permissions: string[];
};

Both are common. The interface version often reads more naturally for hierarchy-like contracts. The type version is often better once composition gets more dynamic or mixes in utility types.

Unions

This is one of the clearest dividing lines. Unions belong to type.

type Theme = 'light' | 'dark' | 'system';

You cannot express a union as an interface. If your model represents alternatives, use a type alias without hesitation.

Intersections

Intersections also fit naturally with type aliases.

type Timestamped = { createdAt: Date; updatedAt: Date };
type User = { id: string; name: string };

type UserRecord = User & Timestamped;

Interfaces can extend other interfaces, but intersections are more flexible when combining object shapes with more advanced constructs.

Mapped and conditional types

Once you are transforming existing types, type is the normal choice.

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type ApiResponse<T> = T extends Error
  ? { ok: false; error: string }
  : { ok: true; data: T };

These are central to advanced TypeScript patterns. Interfaces are not designed for this style of type-level programming.

Class implementation

Classes can implement both interfaces and object-shaped type aliases, but interfaces still read more clearly when you want an explicit contract for a class.

interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(message);
  }
}

This is not because type aliases fail here. It is because implements InterfaceName is widely understood as a contract boundary.

Declaration merging

Interfaces support declaration merging; type aliases do not.

interface Preferences {
  theme: string;
}

interface Preferences {
  language: string;
}

The merged result includes both properties. This can be helpful in framework typing, ambient declarations, and plugin ecosystems. It can also cause confusion if overused. Treat it as a deliberate feature, not a general-purpose habit.

Error readability and editor experience

In practice, editor hints and error messages can differ based on how a type was constructed. A simple interface can sometimes produce cleaner displayed shapes, while heavily composed type aliases can expose more of the machinery behind the scenes.

This is not a hard rule, and TypeScript keeps improving here. Still, when a type is public and likely to appear in many errors, a straightforward interface can reduce mental overhead.

If debugging types is already a pain point in your project, it helps to keep your public contracts simple and your type-level logic isolated. For related debugging strategies, see TypeScript Error Codes List: Meaning, Common Causes, and Fixes and How to Fix "Cannot find name" and Other Missing Type Errors in TypeScript.

Interface vs type for React props

React codebases often use both. There is no universal rule, but a stable team convention helps.

Use interface for exported props that represent clear component contracts:

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

Use type when props are composed from unions, variants, or utility types:

type ButtonProps = {
  label: string;
} & (
  | { href: string; onClick?: never }
  | { href?: never; onClick: () => void }
);

That distinction scales well in frontend work. If you are setting up a React project, the broader environment matters too; see React + TypeScript Setup Guide for Vite, Next.js, and CRA Alternatives.

Best fit by scenario

If you want a practical default, use this decision guide.

Use interface when:

  • You are defining a public object contract.
  • You expect the shape to be extended.
  • You want a class to implement the contract.
  • You are working with framework or library augmentation.
  • The type is simple enough that readability matters more than expressiveness.

Examples:

  • API request and response object shapes
  • service contracts
  • component props with straightforward fields
  • domain entities
  • shared DTO-style structures

Use type when:

  • You need a union or intersection.
  • You are aliasing a primitive, tuple, or function type.
  • You are using mapped, conditional, or utility-heavy types.
  • You are composing types from existing pieces.
  • The type exists mainly to support type-level logic.

Examples:

  • discriminated unions
  • variant-based component props
  • helper generics
  • utility wrappers around existing models
  • derived state or configuration types

A reasonable team standard

Many teams benefit from this simple rule set:

  1. Default to interface for exported object shapes.
  2. Use type for everything interfaces do not express well.
  3. Do not convert one to the other without a clear readability or maintainability reason.

That avoids style churn. It also prevents the common anti-pattern where a codebase swings entirely to one construct for ideological reasons.

Examples from common TypeScript work

In a Node.js service, you might use interfaces for request-level contracts and type aliases for result unions:

interface UserRecord {
  id: string;
  email: string;
}

type SaveUserResult =
  | { ok: true; user: UserRecord }
  | { ok: false; reason: 'duplicate-email' | 'db-error' };

In a frontend app, you might use an interface for a fetched entity and a type alias for UI state:

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

type ProjectState =
  | { status: 'loading' }
  | { status: 'error'; message: string }
  | { status: 'ready'; project: Project };

That split is easy to teach and easy to maintain.

As your project grows, consistency in linting and config also matters. Related guides that help keep those conventions stable include ESLint + TypeScript Flat Config Guide and tsconfig.json Best Settings by Project Type.

When to revisit

This topic is worth revisiting from time to time, not because the basics change every month, but because TypeScript itself keeps improving and ecosystem conventions shift.

You should review your team guidance on interface vs type when any of the following happens:

  • Your codebase starts using more advanced utility types. A project that began with simple object contracts may later rely heavily on unions, mapped types, and generics.
  • You adopt a new framework or runtime pattern. React, Next.js, Node.js services, and validation libraries can each influence how often you reach for one construct over the other.
  • Your public APIs become more stable. Once contracts harden, you may prefer interfaces for clarity even if earlier prototypes used type aliases everywhere.
  • Your team struggles with readability. If type errors are too dense or PRs often debate style, simplify the rule set.
  • TypeScript releases add or refine behavior. Small language changes can make previous conventions feel less useful or less necessary.

The action step here is simple: write down a lightweight convention in your project docs. Keep it short enough that people will actually follow it.

For example:

# Interface vs Type
- Use `interface` for exported object contracts.
- Use `type` for unions, tuples, aliases, mapped types, and conditional types.
- Prefer the option that makes public APIs easiest to read.
- Avoid refactoring between them unless it improves clarity.

That gives your team a default without turning the choice into a code review argument.

If you want one final practical takeaway, use this:

  • Choose interface for shape-first modeling.
  • Choose type for expression-first modeling.

That distinction stays useful across most TypeScript versions and project types.

And if you are migrating from JavaScript, do not let this decision block progress. Both tools are valid. Start with clear names, strict enough compiler settings, and a consistent team habit. The better long-term win is not picking the “perfect” keyword. It is building a codebase where types explain intent instead of hiding it.

Related Topics

#interfaces#types#best-practices#typescript-fundamentals#language
T

TypeScript Page Editorial

Senior SEO Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

2026-06-17T08:55:28.262Z