When to Use Type and When to Use Interface

When to Use Type and When to Use InterfaceWhen to Use Type and When to Use Interface

Nov 28, 2025 - 13 min

Roko Ponjarac

Roko Ponjarac

Software Engineer


Type and interface are the two main tools you can use in TypeScript to define the shapes of objects. Both do similar things, but they each have their own strengths. This guide explains when to use one over the other and gives you practical code examples that you can use in real projects.

Getting the Basics Down

Let's first figure out what each construct does and how they are different at a basic level before we start comparing them.

What is a Type Alias?

A type alias gives a name to any type. You set the rules for primitives, unions, tuples, objects, and more complicated combinations. The type keyword lets you do things that interfaces can't.

// Union type — can't be done with interface
type Status = 'pending' | 'approved' | 'rejected';

// Tuple type
type Coordinates = [number, number];

// Function type
type Validator = (value: string) => boolean;

Type aliases are great at showing data shapes that aren't objects. When your app needs union types to handle state or API responses, types are very important.

What is an Interface?

An interface sets the rules for how an object should be structured. Interfaces come from classical object-oriented programming and are still used in TypeScript.

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

interface Product {
  id: string;
  name: string;
  description?: string;
  price: number;
}

Interfaces only care about the shapes of objects. Developers who know Java, C#, or other OOP languages will find their syntax familiar.

The Main Differences Between Type and Interface

Knowing the technical differences helps you make smart choices in your code.

Declaration Merging

You can merge declarations with interfaces. TypeScript takes several interface declarations with the same name and combines them into one definition. Types don't let this happen.

// Merging interface declarations
interface Window {
  customProperty: string;
}

interface Window {
  anotherProperty: number;
}
// Window now has both properties

// Type can't be declared again — Error!
type Response = { data: string };
type Response = { error: string }; // Error: Duplicate identifier

When you want to add to the types in a third-party library, declaration merging can be helpful. This pattern lets you add to your own theme when you work with Material UI theming.

Extending and Intersecting

Both constructs allow for extension, but the syntax is different. The extends keyword is used by interfaces. Types make use of intersection operators.

// Interface extension
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

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

Union Types

Types handle unions. Interfaces can't directly make unions.

type Result<T> = { success: true; data: T } | { success: false; error: string };

type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

A lot of the time, this pattern is used in React apps for state machines and API responses.

Mapped and Computed Types

Types can have computed properties and mapped types that let you create new types on the fly.

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

type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
// Output: "onClick" | "onFocus" | "onBlur"

Rules for Real-World Projects

Consistent rules are good for production codebases. Here's a method that has been tried and true in enterprise React apps.

The Choice Matrix

The table below shows when to use each construct:

Use CaseRecommendation
Data models (User, Product, Order)interface
Component propsinterface
Store state (Redux, Zustand, Valtio)interface
API response typestype
Form valuestype
Union typestype
Function typestype
Intersection typestype

This matrix is based on one rule: use interface for structures that show "what something looks like" and type for "what gets sent or received."

Data Models with Interface

Data models show how things in your app are related to each other. Interfaces are good for this because they describe the structures of objects and allow for extension.

export interface UserModel {
  id: number;
  fullName: string;
  email: string;
  roles: UserRole[];
  userStatus: UserStatus;
}

export interface RecordsModel {
  id: number;
  userId: number;
  clockInDate: string;
  clockOutDate: string;
}

When models have the same fields, interface extension keeps code DRY:

export interface BaseEntity {
  id: number;
  createdAt: string;
  updatedAt: string;
}

export interface RecordsModel extends BaseEntity {
  userId: number;
  clockInDate: string;
}

API Response Types with Type

API boundaries are about changing data. Types take care of the complicated patterns that these situations need.

export type PayloadResponse<T> = {
  payload: T;
  message?: string;
};

export type PaginatedResponse<T> = {
  entities: T[];
  totalCount: number;
  pagination?: {
    pageNumber: number;
    pageSize: number;
  };
};

Form Values with Type

Before validation, form values show what the user typed in. Types work well because many times, forms need union types for their select fields.

export type LoginFormValues = {
  email: string;
  password: string;
};

export type CreateUserFormValues = {
  fullName: string;
  email?: string;
  department: 'sales' | 'engineering' | 'marketing';
};

Store State with Interface

State management stores determine the structure of application state. Interfaces make this clear.

interface AuthStore {
  user: UserModel | null;
  authenticating: boolean;
  token: string | null;
}

interface UsersStore {
  users: UserModel[];
  selectedUser?: UserModel;
  isLoading: boolean;
  totalCount: number;
}

Real-World Examples Using React and Next.js

In frontend development, there are certain situations where it matters whether you use a type or an interface.

Component Props

Both work to set props for React components. The community pattern prefers using interfaces for props.

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode;
}

function Button({ variant, size = 'md', onClick, children }: ButtonProps) {
  return (
    <button className={`btn btn-${variant} btn-${size}`} onClick={onClick}>
      {children}
    </button>
  );
}

Intersection types are great for extending HTML element props:

type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string;
  error?: string;
};

Function and Callback Types

Function signatures should have their own types. This pattern makes it easier to read.

export type FormSubmitHandler<T> = (data: T, methods: UseFormReturn<T>) => void | Promise<void>;

type ValidationFn<T = string> = (value?: T) => true | string;

Global Type Patterns

Internationalization

Typed translation keys stop runtime errors when you make React apps that work in more than one language.

type TranslationNamespace = 'common' | 'auth' | 'dashboard' | 'errors';

interface LocaleConfig {
  code: string;
  name: string;
  direction: 'ltr' | 'rtl';
}

This method works well in Next.js translation setups that make sure you don't miss any translation keys by keeping type safety.

Markdown and Static Content

Static site generators and blog systems need to be able to handle typed content. Here's how types and interfaces work together for static pages made with Markdown.

// Union type — use type
export type BlogCategory = 'development' | 'design' | 'marketing';

// Data structure — use interface
export interface BlogPost {
  slug: string;
  title: string;
  date: string;
  category: BlogCategory;
  featured: boolean;
}

// Extended structure — use interface
export interface BlogPostWithContent extends BlogPost {
  content: string;
}

Look at the pattern: BlogCategory uses type because it is a union. BlogPost uses interface because it is a way to show a data structure.

Best Practices and Advanced Patterns

Conditional Types

Types allow for conditional logic that interfaces can't show.

type ExtractArrayType<T> = T extends Array<infer U> ? U : never;

type StringArray = string[];
type ExtractedString = ExtractArrayType<StringArray>; // string

Generic Constraints

Both support generics, but types let you set more flexible limits.

type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

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

type StringKeys = KeysOfType<User, string>; // "name" | "email"

Utility Type Patterns

Make utility types that can be used over and over again for common changes.

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

// Usage: Make "id" optional for creation
type CreateUserDTO = PartialBy<User, 'id'>;

Things You Shouldn't Do

Mixing Styles Without Reason

Choose a convention and stick to it. Using things differently confuses team members.

// Bad: Mixed approach without a reason
type UserProps = { name: string };
interface ProductProps { name: string }

// Good: Consistent approach
interface UserProps { name: string }
interface ProductProps { name: string }

Overusing Type

Types give you more options, but interfaces are better for object structures because they make your intentions clearer.

// Not the best: Type for a simple object
type User = { id: string; name: string };

// Better: Interface signals this is a data structure
interface User {
  id: string;
  name: string;
}

Not Using Declaration Merging

Use interfaces to take advantage of declaration merging when adding new types to third-party code.

// Extending Express Request
declare global {
  namespace Express {
    interface Request {
      user?: UserModel;
    }
  }
}

Performance Considerations

The performance of TypeScript compilation varies slightly between types and interfaces. Interfaces make one flat object type. More calculations are needed to find the intersection of complex types.

For most uses, this difference doesn't matter much. Put more emphasis on code clarity than on small improvements.

Directory Structure for Types

Put types in order by what they do:

src/
├── types/                    # Use type — API types and response types
│   ├── response.type.ts
│   └── blog.type.ts
├── models/                   # Use interface — data models
│   ├── user.model.ts
│   └── record.model.ts
├── components/               # Use interface — component props
└── stores/                   # Use interface — store state

This structure shows what you want to do. Developers know what to expect in every folder.

The Reason for the Choice

The difference is in the words. Use type for types that tell you "what gets sent or received" (API, forms, callbacks). Use interface for things that tell you "how something looks," like models, props, and store state.

This way of thinking is in line with how TypeScript grew. OOP customs led to the creation of interfaces for setting up object contracts. Types were made to handle the changing patterns in JavaScript, like unions and mapped types.

Ready to talk?

Send a brief introduction to schedule a discovery call. The call focuses on your challenges and goals and outlines the first steps toward the right digital solution.

When to Use Type and When to Use Interface | Workspace