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 Case | Recommendation |
|---|---|
| Data models (User, Product, Order) | interface |
| Component props | interface |
| Store state (Redux, Zustand, Valtio) | interface |
| API response types | type |
| Form values | type |
| Union types | type |
| Function types | type |
| Intersection types | type |
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.







