TypeScript Interface vs Type: When to Use Which?

TypeScript gives you two main ways to define object shapes: interfaces and types. While they’re similar, understanding their differences helps you write better, more maintainable code. In this post, we’ll break down when to use each one.

Introduction to TypeScript Interfaces and Types

Core Concepts - Interfaces and Types

Interfaces define object shapes and can be extended or implemented by classes. Types are type aliases that can represent any valid TypeScript type, including primitives, unions, and intersections.

// Interface
interface User {
    name: string;
    age: number;
}

// Type alias
type UserType = {
    name: string;
    age: number;
};

Fundamental Differences Between Interfaces and Types

Syntax and Declaration

Interface syntax:

interface Person {
    name: string;
    age: number;
    greet(): void;
}

// Extending interfaces
interface Employee extends Person {
    employeeId: string;
    department: string;
}

Type alias syntax:

type Person = {
    name: string;
    age: number;
    greet(): void;
};

// Extending types (using intersection)
type Employee = Person & {
    employeeId: string;
    department: string;
};

Key differences:

  • Interfaces use the interface keyword, types use type
  • Interfaces can only describe object shapes, types can be anything
  • Interfaces support declaration merging, types don’t

Primitive and Complex Types

Interfaces are limited to objects:

// ✅ Valid interface - it represents the shape of an object
interface Config {
    apiUrl: string;
    timeout: number;
}

// ❌ Invalid - interfaces can't represent primitives directly
// interface StringType = string;

// ❌ Invalid - interfaces can't represent unions directly
// interface StringOrNumber = string | number;

Types can represent anything (both primitives and object shapes):

// ✅ Valid type aliases
type StringType = string;
type NumberType = number;
type StringOrNumber = string | number;
type TupleType = [string, number];
type FunctionType = (x: number) => string;

// ✅ Complex type compositions
type Status = 'loading' | 'success' | 'error';
type ApiResponse<T> = {
    data: T;
    status: Status;
    message?: string;
};

Object and Function Descriptions

Describing objects:

// Both work equally well for simple objects
interface User {
    id: number;
    name: string;
    email: string;
}

type UserType = {
    id: number;
    name: string;
    email: string;
};

// Function types
interface MathFunc {
    (x: number, y: number): number;
}

type MathFuncType = (x: number, y: number) => number;

Function overloads:

// Interfaces support function overloads
interface Calculator {
    add(x: number, y: number): number;
    add(x: string, y: string): string;
}

// Types can also represent overloads
type CalculatorType = {
    add(x: number, y: number): number;
    add(x: string, y: string): string;
};

Advanced Features and Capabilities

Declaration Merging

Interfaces support declaration merging:

interface User {
    name: string;
}

interface User {
    age: number;
}

// Result: User has both name and age
const user: User = {
    name: "John",
    age: 30
};

Types don’t support merging:

type User = {
    name: string;
};

// ❌ Error: Duplicate identifier 'User'
// type User = {
//     age: number;
// };

Practical use cases for merging:

// Extending library types
interface Window {
    myCustomProperty: string;
}

// Now window.myCustomProperty is available
window.myCustomProperty = "Hello";

Extending and Implementing

Extending interfaces:

interface Animal {
    name: string;
    makeSound(): void;
}

interface Dog extends Animal {
    breed: string;
    bark(): void;
}

const myDog: Dog = {
    name: "Buddy",
    breed: "Golden Retriever",
    makeSound() {
        this.bark();
    },
    bark() {
        console.log("Woof!");
    }
};

Extending types (using intersection):

type Animal = {
    name: string;
    makeSound(): void;
};

type Dog = Animal & {
    breed: string;
    bark(): void;
};

Class implementation:

interface Vehicle {
    start(): void;
    stop(): void;
}

class Car implements Vehicle {
    start() {
        console.log("Car starting...");
    }
    
    stop() {
        console.log("Car stopping...");
    }
}

// Types can also be implemented
type VehicleType = {
    start(): void;
    stop(): void;
};

class Motorcycle implements VehicleType {
    start() {
        console.log("Motorcycle starting...");
    }
    
    stop() {
        console.log("Motorcycle stopping...");
    }
}

Specialized Type Features

Mapped types (types only):

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

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

type ReadonlyUser = Readonly<User>;
// Result: { readonly name: string; readonly age: number; }

Conditional types (types only):

type NonNullable<T> = T extends null | undefined ? never : T;

type StringOrNull = string | null;
type NonNullString = NonNullable<StringOrNull>; // string

Utility types (types only):

type Partial<T> = {
    [P in keyof T]?: T[P];
};

type Required<T> = {
    [P in keyof T]-?: T[P];
};

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Performance and Tooling Considerations

Compilation and Runtime Performance

Compilation time:

  • Interfaces are generally faster to compile
  • Types with complex unions/intersections can slow compilation
  • Large projects benefit from interface performance

Runtime performance:

  • No runtime difference - both are erased during compilation
  • TypeScript is a development tool, not a runtime library

Developer Experience and Tooling

IDE support:

  • Both have excellent IntelliSense support
  • Interfaces show clearer error messages for missing properties
  • Types provide better support for complex type operations

Error messaging:

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

const user: User = {
    name: "John"
    // Error: Property 'age' is missing in type '{ name: string; }'
};

type UserType = {
    name: string;
    age: number;
};

const userType: UserType = {
    name: "John"
    // Similar error message
};

Best Practices and Usage Guidelines

When to Use Interfaces

Use interfaces for:

  • Object-oriented design patterns
  • API contracts and public interfaces
  • When you need declaration merging
  • Simple object shapes that might be extended
// Good interface use case
interface ApiResponse<T> {
    data: T;
    status: 'success' | 'error';
    message?: string;
}

interface UserApiResponse extends ApiResponse<User> {
    user: User;
}

Object-oriented patterns:

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

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

When to Use Types

Use types for:

  • Complex type compositions
  • Union and intersection types
  • Mapped and conditional types
  • Function types and overloads
  • When you need to represent non-object types
// Good type use case
type Status = 'idle' | 'loading' | 'success' | 'error';

type ApiState<T> = {
    data: T | null;
    status: Status;
    error: string | null;
};

type AsyncFunction<T> = (...args: any[]) => Promise<T>;

Complex type operations:

type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

type EventMap = {
    click: MouseEvent;
    keydown: KeyboardEvent;
    submit: Event;
};

type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;

Hybrid Approaches

Combining both effectively:

// Use interfaces for public APIs
interface UserService {
    getUser(id: number): Promise<User>;
    createUser(user: CreateUserRequest): Promise<User>;
}

// Use types for internal implementations
type CreateUserRequest = Omit<User, 'id' | 'createdAt'>;
type UserUpdateRequest = Partial<CreateUserRequest>;

// Use types for utility functions
type ValidationResult<T> = {
    isValid: boolean;
    errors: string[];
    data?: T;
};

Real-world Applications and Case Studies

Frontend Development

React component props:

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

const Button: React.FC<ButtonProps> = ({ children, variant, size, onClick, disabled }) => {
    // Component implementation
};

State management:

interface AppState {
    user: User | null;
    isLoading: boolean;
    error: string | null;
}

type AppAction = 
    | { type: 'SET_USER'; payload: User }
    | { type: 'SET_LOADING'; payload: boolean }
    | { type: 'SET_ERROR'; payload: string | null };

Backend Development

Express.js with TypeScript:

interface RequestBody {
    name: string;
    email: string;
    age?: number;
}

interface ApiResponse<T> {
    success: boolean;
    data?: T;
    error?: string;
}

app.post('/users', (req: Request<{}, {}, RequestBody>, res: Response<ApiResponse<User>>) => {
    // Route implementation
});

Database models:

interface User {
    id: number;
    name: string;
    email: string;
    createdAt: Date;
    updatedAt: Date;
}

type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateUserInput = Partial<CreateUserInput>;

Full-stack TypeScript Projects

Shared types:

// shared/types.ts
export interface User {
    id: number;
    name: string;
    email: string;
}

export type UserRole = 'admin' | 'user' | 'moderator';

export interface ApiResponse<T> {
    data: T;
    status: 'success' | 'error';
    message?: string;
}

Conclusion and Best Practices Summary

Recap of Key Differences

Feature Interface Type
Object shapes
Primitives
Unions/Intersections
Declaration merging
Mapped types
Conditional types
Performance ⚠️

Decision-making Framework

Choose Interface when:

  • Defining object shapes for classes
  • Creating public APIs
  • You might need declaration merging
  • Working with object-oriented patterns

Choose Type when:

  • You need unions or intersections
  • Working with primitives
  • Creating complex type compositions
  • Using advanced TypeScript features

Hybrid approach:

  • Use interfaces for public contracts
  • Use types for internal implementations
  • Use types for utility and helper types

Final Thoughts

Both interfaces and types are powerful tools in TypeScript. The key is understanding their strengths and using the right tool for the job. Start with interfaces for simple object shapes, and reach for types when you need more flexibility.

If this article was helpful, tweet it!