TypeScript Interfaces
Interfaces define contracts for object shapes, specifying what properties and methods an object must have. They're essential for defining APIs, component contracts, and establishing consistent data structures across an application. Interfaces support inheritance, method signatures, and index signatures.
Explained in 2 Minutes: Type vs Interface In Typescript
©onjsdev
Basic Interface Syntax
An interface is like a blueprint that describes what properties and methods an object should have:
interface User {
id: number;
name: string;
email: string;
}
// Creating an object that matches the interface
const john: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
};
Optional Properties
Optional properties in TypeScript use the question mark syntax, ?
, to indicate that a property may or may not exist on an object. They're essential for modeling real-world data where certain fields might be missing, like user profiles where some information is optional, API responses with conditional fields, or configuration objects with default values. Optional properties affect type checking by allowing objects to omit these properties entirely while still maintaining type safety for when they are present.
interface Product {
id: number;
name: string;
price: number;
description?: string; // Optional property
}
// Both of these are valid
const laptop: Product = {
id: 1,
name: "Laptop",
price: 999,
};
const phone: Product = {
id: 2,
name: "Phone",
price: 699,
description: "Latest smartphone model",
};
Readonly Properties
Readonly properties prevent modification after initial assignment, creating immutable object structures. They're crucial for functional programming patterns, preventing accidental mutations, and ensuring data integrity in shared objects.
Readonly properties help catch bugs at compile time when code attempts to modify data that should remain constant, like configuration settings, cached results, or immutable state objects.
interface Point {
readonly x: number;
readonly y: number;
}
const point: Point = { x: 10, y: 20 };
// point.x = 5; // Error: Cannot assign to 'x' because it is a read-only property
Function Types in Interfaces
Function types within interfaces define the signatures of methods or callable properties. They specify parameter types, return types, and optional parameters for functions that belong to objects. This enables type-safe callback definitions, event handler specifications, and API method contracts. Function types can be overloaded to support multiple call signatures for flexible APIs.
interface CalculateTotal {
(price: number, quantity: number, taxRate: number): number;
}
const calculateTotal: CalculateTotal = (price, quantity, taxRate) => {
return price * quantity * (1 + taxRate);
};
Method Definitions
Method definitions in interfaces describe functions that belong to objects using method syntax rather than property syntax. They're functionally similar to function types but provide cleaner syntax for object-oriented designs. Method definitions support generic parameters, overloads, and can represent constructors, regular methods, or static methods depending on context.
interface Logger {
log(message: string): void;
error(message: string): void;
}
const consoleLogger: Logger = {
log(message) {
console.log(`LOG: ${message}`);
},
error(message) {
console.error(`ERROR: ${message}`);
},
};
Extending Interfaces
Interfaces can extend other interfaces to inherit their properties:Interface extension creates hierarchical type relationships where child interfaces inherit all properties from parent interfaces while adding their own. This enables building complex type hierarchies, sharing common properties across related types, and creating specialized versions of base interfaces.
Extension supports multiple inheritance, allowing interfaces to extend from several parent interfaces simultaneously
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
const employee: Employee = {
name: "Alice",
age: 30,
employeeId: 123,
department: "Engineering",
};
Types vs. Interfaces
Types
and interfaces
serve similar purposes but have important differences. Interfaces are specifically designed for object shapes and support declaration merging, extension, and are generally preferred for defining object contracts and public APIs.
Types are more flexible, supporting any type definition including primitives, unions, intersections, and computed types, but cannot be reopened or merged. Interfaces are better for library definitions and extensible APIs, while types excel at complex type transformations, utility types, and situations requiring union or intersection types.
The choice often depends on whether you need the object-oriented features of interfaces or the functional programming flexibility of types.
Basic Syntax Comparison
// Interface
interface User {
name: string;
age: number;
}
// Type
type User = {
name: string;
age: number;
};
Key Differences
1. Declaration Merging
When you declare multiple interfaces with the same name, TypeScript automatically merges them into a single interface containing all properties from each declaration. This is particularly powerful for extending global objects, augmenting third-party library types, or building modular type definitions where different parts of your application can add properties to the same interface.
The merging happens automatically and the resulting interface includes all properties with proper type checking for conflicts.
// Interface merging
interface User {
name: string;
}
interface User {
age: number;
}
// The User interface now has both name and age
const user: User = {
name: "John",
age: 30,
};
Types Cannot Be Merged: Type aliases with the same name create conflicts and compilation errors. Each type alias must have a unique name within its scope, making them unsuitable for scenarios where you need to incrementally build up type definitions across multiple files or modules.
type User = {
name: string;
};
// Error: Duplicate identifier 'User'
// type User = {
// age: number;
// };
2. Extending
Interfaces use the extends
keyword to inherit from other interfaces, creating clear hierarchical relationships. They support multiple inheritance, allowing an interface to extend from several parent interfaces simultaneously. This creates clean, object-oriented type hierarchies that mirror class inheritance patterns.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
Types achieve extension through intersection types using the ampersand & operator. While functionally similar to interface extension, the syntax is different and the resulting types are computed rather than declared. Types can extend from both other types and interfaces, providing more flexibility in composition.
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
3. Additional Capabilities of Types
Types Handle Non-Object Structures: Unlike interfaces which are limited to object shapes, types can represent any TypeScript type including primitives, unions, intersections, tuples, function signatures, string literals, and complex computed types.
This makes types the only option for creating aliases for simple values, modeling data that can be multiple different shapes, performing type-level computations with conditional and mapped types, and working with any non-object type structure that TypeScript supports.
// Union types
type ID = string | number;
// Literal types
type Direction = "north" | "south" | "east" | "west";
// Type aliases for primitives
type Age = number;
// Mapped types
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
When to Use Which?
Use Interfaces When:
You're defining objects or classes that implement a contract:
interface Repository {
findById(id: string): unknown;
save(data: unknown): void;
}
class UserRepository implements Repository {
// Implementation here
}
You want to allow declaration merging:
// Useful when extending libraries or working with declaration files
interface Window {
customProperty: string;
}
You're working with object shapes for data:
interface User {
id: number;
name: string;
}
Use Types When:
You need unions, intersections, or tuples:
type Result = Success | Error;
type Coordinates = [number, number];
You want to use primitives or literals:
type ID = string;
type Status = "pending" | "completed" | "failed";
You need to manipulate types:
type UserKeys = keyof User;
type PartialUser = Partial<User>;
Practical Examples
React Component Props
// Props interface for a Button component
interface ButtonProps {
text: string;
onClick: () => void;
color?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
function Button({ text, onClick, color = "primary", disabled = false }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${color}`}
>
{text}
</button>
);
}
// Usage
<Button
text="Click me"
onClick={() => alert("Clicked!")}
color="primary"
/>
Form Data Interface
interface FormData {
username: string;
password: string;
rememberMe?: boolean;
}
function submitForm(data: FormData) {
console.log("Submitting:", data);
// API call to submit the form
}
const loginData: FormData = {
username: "john_doe",
password: "secure_password",
rememberMe: true,
};
submitForm(loginData);
Best Practices
1. Use PascalCase for naming interfaces:
PascalCase follows TypeScript and JavaScript conventions for types and classes, creating consistency across your codebase. It immediately signals that you're dealing with a type rather than a variable or function, improving code readability and maintaining alignment with community standards and popular style guides.
interface UserProfile { ... } // Good
interface userProfile { ... } // Not ideal
2. Don't use the "I" prefix:
The "I"
prefix is a legacy convention from older programming languages that distinguished interfaces from implementations. Modern TypeScript doesn't need this distinction since interfaces are purely compile-time constructs. Omitting the prefix creates cleaner, more readable code and follows current TypeScript community practices.
interface User { ... } // Good
interface IUser { ... } // Not recommended
3. Keep interfaces focused on a single concept:
Single-responsibility interfaces are easier to understand, test, and maintain. They promote composition over inheritance, make your types more flexible and reusable, and prevent bloated interfaces that become difficult to implement. Focused interfaces also make it easier to evolve your types independently as requirements change.
// Better to have two separate interfaces
interface User { ... }
interface UserPreferences { ... }
// Instead of one large interface
// interface UserWithPreferences { ... }
4. Use readonly for immutable properties:
Readonly properties prevent accidental mutations and clearly communicate intent about data that shouldn't change. This catches bugs at compile time, supports functional programming patterns, and makes your code more predictable by establishing clear boundaries between mutable and immutable data.
interface Config {
readonly apiKey: string;
readonly serverUrl: string;
}
5. Document your interfaces with JSDoc:
JSDoc comments provide essential context that type signatures alone cannot convey. They explain business logic, document constraints and assumptions, provide examples of valid values, and create better developer experience through IDE tooltips. Good documentation makes interfaces self-explanatory and reduces the need for developers to hunt through implementation code to understand usage patterns.
/**
* Represents a user in the system
*/
interface User {
/** Unique identifier */
id: number;
/** User's full name */
name: string;
}
6. Additional best practices
Prefer Composition Over Deep Inheritance
Build complex interfaces by combining simpler ones rather than creating deep inheritance hierarchies, which improves flexibility and maintainability.
Use Generic Constraints Wisely:
When creating generic interfaces, use constraints to provide better type safety and clearer API contracts without over-constraining usage.
Group Related Interfaces
Organize related interfaces in the same file or namespace to improve discoverability and maintain logical code organization.
Avoid Any in Interface Definitions
Using any defeats the purpose of TypeScript's type safety, so prefer unknown, proper unions, or generic types when the exact type is uncertain.
Exercises
Exercise 1: Basic Interface
Create an interface for a Book
with these properties:
title
(string)author
(string)pages
(number)isPublished
(boolean)genres
(array of strings)
Then create a few book objects using this interface.
Exercise 2: Interface with Optional Properties
Create an interface for a UserProfile
with these properties:
id
(number)username
(string)email
(string)bio
(string, optional)age
(number, optional)isPremium
(boolean, optional)
Create a function that prints user information, with special handling for optional properties.
Exercise 3: Interfaces with Methods
Create an interface for a Calculator
with these methods:
add(a: number, b: number): number
subtract(a: number, b: number): number
multiply(a: number, b: number): number
divide(a: number, b: number): number
Then implement this interface with a class.