Table of Contents
- What are Interfaces?
- What are Type Aliases?
- Key Similarities Between Interfaces and Type Aliases
- Key Differences Between Interfaces and Type Aliases
- When to Use Interfaces vs. Type Aliases
- Advanced Use Cases
- Common Pitfalls
- Conclusion
- References
What are Interfaces?
An interface in TypeScript defines a contract for the structure of an object. It specifies the properties, methods, and their types that an object must have to conform to the interface. Interfaces are purely TypeScript constructs (they don’t exist at runtime) and are used to enforce consistency across your codebase.
Basic Syntax
Interfaces are declared with the interface keyword, followed by a name and a body defining the structure:
interface User {
name: string;
age: number;
email?: string; // Optional property (denoted with `?`)
readonly id: string; // Read-only property (can't be modified after initialization)
}
// Example usage
const user: User = {
name: "Alice",
age: 30,
id: "user-123"
// email is optional, so it can be omitted
};
Key Features of Interfaces
1. Optional Properties
Use ? to mark properties that may or may not exist:
interface Config {
apiUrl: string;
timeout?: number; // Optional: defaults to undefined if missing
}
2. Read-Only Properties
Use readonly to prevent modification after initialization:
interface Point {
readonly x: number;
readonly y: number;
}
const point: Point = { x: 10, y: 20 };
point.x = 30; // ❌ Error: Cannot assign to 'x' because it is a read-only property
3. Function Types
Interfaces can define function signatures, ensuring functions adhere to a specific input/output contract:
interface GreetFunction {
(name: string, title?: string): string; // (parameters) => return type
}
const greet: GreetFunction = (name, title = "Mr.") => {
return `Hello, ${title}${name}!`;
};
greet("Doe"); // ✅ "Hello, Mr.Doe!"
4. Indexable Types
Define objects/arrays that can be indexed (e.g., arrays, dictionaries):
// Indexable by string (dictionary-like object)
interface StringMap {
[key: string]: string; // Key is string, value is string
}
const translations: StringMap = {
en: "Hello",
fr: "Bonjour"
};
translations["es"] = "Hola"; // ✅ Valid
translations[0] = "Ciao"; // ❌ Error: Index signature expects string key
// Indexable by number (array-like)
interface NumberArray {
[index: number]: number; // Key is number, value is number
length: number; // Arrays also have a `length` property
}
const scores: NumberArray = [90, 85, 95];
scores[0] = 100; // ✅ Valid
scores.length = 5; // ✅ Valid
5. Extending Interfaces
Interfaces can inherit properties/methods from other interfaces using extends, promoting code reuse:
interface Person {
name: string;
age: number;
}
// Employee extends Person, adding role and department
interface Employee extends Person {
role: string;
department: string;
}
const emp: Employee = {
name: "Bob",
age: 35,
role: "Developer",
department: "Engineering"
};
6. Implementing Interfaces with Classes
Classes can implement interfaces to enforce they adhere to the interface’s contract:
interface Logger {
log(message: string): void;
error(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG]: ${message}`);
}
error(message: string): void { // Must implement all interface methods
console.error(`[ERROR]: ${message}`);
}
}
const logger = new ConsoleLogger();
logger.log("System started"); // ✅ [LOG]: System started
What are Type Aliases?
A type alias (declared with type) creates a new name for an existing type. Unlike interfaces, type aliases are not limited to object shapes—they can represent primitives, unions, intersections, tuples, and more.
Basic Syntax
Type aliases are declared with the type keyword:
// Object shape (similar to an interface)
type User = {
name: string;
age: number;
email?: string;
readonly id: string;
};
// Primitive type
type ID = string | number; // Union of string and number
// Tuple type (fixed-length array with specific types)
type Coordinates = [number, number]; // [x, y]
Key Features of Type Aliases
1. Primitive Types
Type aliases can simplify primitive types or unions of primitives:
type Age = number;
type UUID = string;
type Status = "active" | "inactive" | "pending"; // String union (discriminated union)
const userStatus: Status = "active"; // ✅ Valid
const invalidStatus: Status = "deleted"; // ❌ Error: Type '"deleted"' is not assignable to type 'Status'
2. Union Types
Combine multiple types into one using | (a value can be of any of the types):
type Input = string | number | boolean;
function formatInput(input: Input): string {
if (typeof input === "string") return input.toUpperCase();
if (typeof input === "number") return input.toFixed(2);
return input ? "Yes" : "No";
}
3. Intersection Types
Combine multiple types into a single type with all properties using &:
type Name = { firstName: string; lastName: string };
type Contact = { email: string; phone: string };
// Person = Name + Contact (has all properties of both)
type Person = Name & Contact;
const person: Person = {
firstName: "Alice",
lastName: "Smith",
email: "[email protected]",
phone: "555-1234"
};
4. Tuple Types
Define fixed-length arrays with specific types for each position:
type RGB = [number, number, number]; // [red, green, blue] values (0-255)
const red: RGB = [255, 0, 0]; // ✅ Valid
const invalidRGB: RGB = [255, 0]; // ❌ Error: Source has 2 element(s) but target requires 3
// Tuple with optional elements (using `?`)
type OptionalTuple = [string, number?];
const tuple1: OptionalTuple = ["hello", 42]; // ✅ Valid
const tuple2: OptionalTuple = ["world"]; // ✅ Valid (second element is optional)
5. Function Types
Like interfaces, type aliases can define function signatures:
type Add = (a: number, b: number) => number;
const add: Add = (x, y) => x + y;
add(2, 3); // ✅ 5
Key Similarities Between Interfaces and Type Aliases
Interfaces and type aliases overlap in many ways, often making them interchangeable for basic use cases:
| Feature | Interfaces | Type Aliases |
|---|---|---|
| Define object shapes | ✅ | ✅ |
Optional properties (?) | ✅ | ✅ |
| Read-only properties | ✅ | ✅ |
| Function types | ✅ | ✅ |
| Extendable | ✅ (via extends) | ✅ (via intersections &) |
Key Differences Between Interfaces and Type Aliases
While similar, interfaces and type aliases have critical differences that impact their use cases:
1. Declaration Merging
Interfaces support declaration merging: multiple interfaces with the same name are automatically merged into one. Type aliases cannot be merged—redeclaring a type alias with the same name throws an error.
Example: Interface Merging
// First declaration
interface Car {
brand: string;
}
// Second declaration (same name) – merges with the first!
interface Car {
model: string;
year: number;
}
// Merged result: Car has brand, model, and year
const myCar: Car = {
brand: "Toyota",
model: "Camry",
year: 2023
}; // ✅ Valid
Type Aliases Cannot Merge
type Car = { brand: string };
type Car = { model: string }; // ❌ Error: Duplicate identifier 'Car'
2. Extending Syntax
Interfaces extend using the extends keyword, while type aliases extend using intersections (&).
Interface Extension
interface Animal {
name: string;
}
interface Dog extends Animal {
bark: () => void;
}
Type Alias Extension (Intersection)
type Animal = { name: string };
type Dog = Animal & { bark: () => void }; // Intersection of Animal and { bark: ... }
3. Support for Primitive/Union/Intersection Types
Type aliases can represent primitives, unions, intersections, and tuples. Interfaces cannot—they are limited to object shapes, function types, and indexable types.
Valid Type Aliases (Invalid Interfaces)
// Primitives
type Age = number; // ✅ Valid (type alias)
interface Age { ... } // ❌ Error: Interface name must be a valid identifier (and can't represent primitives)
// Unions
type Status = "active" | "inactive"; // ✅ Valid (type alias)
interface Status { ... } // ❌ Error: Can't define union via interface
// Tuples
type Pair = [string, number]; // ✅ Valid (type alias)
interface Pair { ... } // ❌ Error: Can't define tuple via interface
4. Reusability with typeof
Type aliases can be created from existing values using typeof, while interfaces cannot.
const user = { name: "Alice", age: 30 };
type User = typeof user; // ✅ Type alias from `typeof user` → { name: string; age: number }
interface User extends typeof user {} // ❌ Error: Interface 'User' cannot extend any type other than a class or interface
5. Error Messages
Type aliases often produce more verbose error messages (e.g., when a type alias is a union or intersection). Interfaces, being simpler, tend to show cleaner, more readable errors.
When to Use Interfaces vs. Type Aliases
Choose based on your use case:
Use Interfaces When…
- You need declaration merging (e.g., extending third-party library types).
- You’re defining a contract for classes (classes implement interfaces with
implements). - You want to extend or be extended by other interfaces (using
extendsfor clarity). - You prefer cleaner error messages and IDE support (interfaces are more idiomatic for object shapes).
Use Type Aliases When…
- You need to represent primitives (e.g.,
type ID = string), unions (e.g.,type Status = "on" | "off"), intersections, or tuples. - You need a single type for multiple related structures (e.g., combining types with
&). - You want to create branded types (e.g.,
type UserID = string & { __brand: "user" }to prevent mixing IDs). - You need conditional types (e.g.,
type NonNullable<T> = T extends null | undefined ? never : T).
Advanced Use Cases
1. Generic Interfaces vs. Generic Type Aliases
Both can work with generics (reusable types with type parameters), but syntax differs slightly:
Generic Interface
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Generic Type Alias
type Box<T> = { value: T };
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
2. Branded Types (Type Aliases Only)
Type aliases enable “branding” to distinguish between semantically different types that share the same underlying structure (e.g., UserID vs. PostID):
// Branded type for User IDs
type UserID = string & { __brand: "user" };
// Branded type for Post IDs
type PostID = string & { __brand: "post" };
// Function that only accepts UserID
function getUser(id: UserID) { /* ... */ }
const userID: UserID = "user-123" as UserID;
const postID: PostID = "post-456" as PostID;
getUser(userID); // ✅ Valid
getUser(postID); // ❌ Error: Argument of type 'PostID' is not assignable to parameter of type 'UserID'
3. Conditional Types (Type Aliases Only)
Type aliases support conditional types, which depend on a condition check:
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
Common Pitfalls
1. Accidental Interface Merging
Unintentionally merging interfaces with the same name can lead to bugs:
// File 1
interface Config {
apiUrl: string;
}
// File 2 (unrelated)
interface Config {
timeout: number; // Merges with File 1's Config!
}
// Now Config requires both apiUrl and timeout – may break existing code
2. Using Interfaces for Primitives/Unions
Interfaces cannot represent primitives or unions, leading to errors:
interface Status {
"active" | "inactive"; // ❌ Error: '{' expected
}
3. Confusing extends with Intersections
Interfaces use extends; type aliases use intersections (&). Mixing them can cause unexpected behavior:
// Interface extending a type alias (works, but less common)
type Animal = { name: string };
interface Dog extends Animal {
bark: () => void;
}
// Type alias intersecting an interface (works)
interface Cat {
meow: () => void;
}
type Pet = Cat & { name: string };
Conclusion
Interfaces and type aliases are foundational to TypeScript’s type system, each with unique strengths. Interfaces excel at defining object contracts, enabling merging, and working with classes. Type aliases shine for primitives, unions, intersections, tuples, and advanced patterns like branded or conditional types.
By understanding their similarities, differences, and use cases, you’ll write more idiomatic, maintainable TypeScript code. Remember: interfaces for object contracts and merging; type aliases for flexibility with primitives and complex types.