Interfaces & Types#
Interfaces and type aliases are two ways to define custom types in TypeScript. Understanding when to use each is crucial for writing clean, maintainable code.
Interfaces#
Basic Interface#
interface User {
id: number;
name: string;
email: string;
}
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com"
};
Optional Properties#
interface Product {
id: number;
name: string;
description?: string; // Optional
price: number;
discount?: number; // Optional
}
const product1: Product = {
id: 1,
name: "Laptop",
price: 999.99
// description and discount are optional
};
const product2: Product = {
id: 2,
name: "Mouse",
description: "Wireless gaming mouse",
price: 49.99,
discount: 10
};
Readonly Properties#
interface Config {
readonly apiUrl: string;
readonly apiKey: string;
timeout: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
apiKey: "secret-key-123",
timeout: 5000
};
config.timeout = 10000; // ✅ OK
// config.apiUrl = "https://new.com"; // ❌ Error: Cannot assign to 'apiUrl' because it is a read-only property
Index Signatures#
// String index signature
interface StringMap {
[key: string]: string;
}
const errors: StringMap = {
email: "Invalid email",
password: "Password too short",
username: "Username taken"
};
// Number index signature
interface NumberArray {
[index: number]: number;
}
const fibonacci: NumberArray = [1, 1, 2, 3, 5, 8, 13];
// Combined with known properties
interface Dictionary {
[key: string]: any;
count: number; // Must be compatible with index signature
toString(): string; // Methods are OK
}
Function Types in Interfaces#
interface MathOperation {
(x: number, y: number): number;
}
const add: MathOperation = (x, y) => x + y;
const multiply: MathOperation = (x, y) => x * y;
// Interface with properties and methods
interface Calculator {
brand: string;
model: string;
calculate(operation: string, x: number, y: number): number;
}
const calculator: Calculator = {
brand: "Casio",
model: "FX-991",
calculate(operation, x, y) {
switch (operation) {
case "add": return x + y;
case "subtract": return x - y;
default: return 0;
}
}
};
Extending Interfaces#
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
const employee: Employee = {
name: "Bob",
age: 30,
employeeId: 12345,
department: "Engineering"
};
// Multiple inheritance
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Article extends Person, Timestamped {
title: string;
content: string;
}
Type Aliases#
Basic Type Alias#
type ID = string | number;
type UserID = string;
type Email = string;
let userId: UserID = "user_123";
let email: Email = "user@example.com";
Object Type Aliases#
type Point = {
x: number;
y: number;
};
type User = {
id: number;
name: string;
email: string;
age?: number;
};
const point: Point = { x: 10, y: 20 };
const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
Union Types#
type Status = "pending" | "approved" | "rejected";
type Result = Success | Error;
type Success = {
type: "success";
data: any;
};
type Error = {
type: "error";
message: string;
};
function handleResult(result: Result) {
if (result.type === "success") {
console.log(result.data);
} else {
console.error(result.message);
}
}
Intersection Types#
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: number;
department: string;
};
type Staff = Person & Employee;
const staff: Staff = {
name: "Charlie",
age: 28,
employeeId: 456,
department: "Marketing"
};
// Combining multiple types
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type Article = {
title: string;
content: string;
} & Timestamped;
Function Type Aliases#
type GreetFunction = (name: string) => string;
type MathOperation = (x: number, y: number) => number;
type Callback<T> = (error: Error | null, data: T | null) => void;
const greet: GreetFunction = (name) => `Hello, ${name}!`;
const add: MathOperation = (x, y) => x + y;
const handleResponse: Callback<User> = (error, data) => {
if (error) {
console.error(error);
} else {
console.log(data);
}
};
Interface vs Type: When to Use Which?#
Use Interface When:#
-
Defining object shapes
interface User { id: number; name: string; } -
You need declaration merging
interface Window { myCustomProperty: string; } interface Window { anotherProperty: number; } // Both declarations merge into one -
Extending or implementing
interface Animal { name: string; } interface Dog extends Animal { breed: string; } class Labrador implements Dog { name = "Buddy"; breed = "Labrador"; }
Use Type When:#
-
Creating union types
type Status = "pending" | "approved" | "rejected"; type ID = string | number; -
Creating intersection types
type Admin = User & { adminLevel: number }; -
Defining primitive aliases
type Email = string; type Age = number; -
Using mapped or conditional types
type Readonly<T> = { readonly [P in keyof T]: T[P] }; type NonNullable<T> = T extends null | undefined ? never : T;
Advanced Patterns#
Discriminated Unions#
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
const circle: Shape = { kind: "circle", radius: 10 };
const square: Shape = { kind: "square", size: 20 };
Generic Interfaces#
interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
class NumberContainer implements Container<number> {
constructor(public value: number) {}
getValue(): number {
return this.value;
}
setValue(value: number): void {
this.value = value;
}
}
// Generic type with constraints
interface Response<T extends { id: number }> {
data: T;
status: number;
error: string | null;
}
Mapped Types#
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
type User = {
id: number;
name: string;
email: string;
};
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string }
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }
Conditional Types#
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical example
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null>; // string
type D = NonNullable<number | undefined>; // number
Practical Examples#
API Response Types#
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
timestamp: number;
}
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "guest";
}
interface PaginatedResponse<T> {
items: T[];
page: number;
pageSize: number;
total: number;
hasMore: boolean;
}
type UserListResponse = ApiResponse<PaginatedResponse<User>>;
async function fetchUsers(): Promise<UserListResponse> {
const response = await fetch("/api/users");
return response.json();
}
Form State Management#
interface FormField<T = string> {
value: T;
error: string | null;
touched: boolean;
dirty: boolean;
}
interface LoginForm {
email: FormField<string>;
password: FormField<string>;
rememberMe: FormField<boolean>;
}
type FormState = {
[K in keyof LoginForm]: LoginForm[K];
};
const initialState: FormState = {
email: { value: "", error: null, touched: false, dirty: false },
password: { value: "", error: null, touched: false, dirty: false },
rememberMe: { value: false, error: null, touched: false, dirty: false }
};
Event System#
interface Event<T = any> {
type: string;
payload: T;
timestamp: number;
}
interface UserLoginEvent extends Event<{ userId: string; ip: string }> {
type: "USER_LOGIN";
}
interface UserLogoutEvent extends Event<{ userId: string }> {
type: "USER_LOGOUT";
}
interface ErrorEvent extends Event<{ message: string; code: number }> {
type: "ERROR";
}
type AppEvent = UserLoginEvent | UserLogoutEvent | ErrorEvent;
function handleEvent(event: AppEvent): void {
switch (event.type) {
case "USER_LOGIN":
console.log(`User ${event.payload.userId} logged in from ${event.payload.ip}`);
break;
case "USER_LOGOUT":
console.log(`User ${event.payload.userId} logged out`);
break;
case "ERROR":
console.error(`Error ${event.payload.code}: ${event.payload.message}`);
break;
}
}
Builder Pattern#
interface QueryBuilder {
select(...fields: string[]): QueryBuilder;
from(table: string): QueryBuilder;
where(condition: string): QueryBuilder;
orderBy(field: string, direction: "ASC" | "DESC"): QueryBuilder;
limit(count: number): QueryBuilder;
build(): string;
}
class SqlQueryBuilder implements QueryBuilder {
private query: {
select: string[];
from: string;
where: string[];
orderBy: string | null;
limit: number | null;
} = {
select: [],
from: "",
where: [],
orderBy: null,
limit: null
};
select(...fields: string[]): QueryBuilder {
this.query.select = fields;
return this;
}
from(table: string): QueryBuilder {
this.query.from = table;
return this;
}
where(condition: string): QueryBuilder {
this.query.where.push(condition);
return this;
}
orderBy(field: string, direction: "ASC" | "DESC"): QueryBuilder {
this.query.orderBy = `${field} ${direction}`;
return this;
}
limit(count: number): QueryBuilder {
this.query.limit = count;
return this;
}
build(): string {
const parts: string[] = [];
parts.push(`SELECT ${this.query.select.join(", ")}`);
parts.push(`FROM ${this.query.from}`);
if (this.query.where.length > 0) {
parts.push(`WHERE ${this.query.where.join(" AND ")}`);
}
if (this.query.orderBy) {
parts.push(`ORDER BY ${this.query.orderBy}`);
}
if (this.query.limit !== null) {
parts.push(`LIMIT ${this.query.limit}`);
}
return parts.join(" ");
}
}
// Usage
const query = new SqlQueryBuilder()
.select("id", "name", "email")
.from("users")
.where("age > 18")
.where("status = 'active'")
.orderBy("name", "ASC")
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name ASC LIMIT 10
Best Practices#
-
✅ Prefer interfaces for object types in public APIs
export interface User { id: number; name: string; } -
✅ Use types for unions, intersections, and utility types
type Status = "active" | "inactive"; type Admin = User & { role: "admin" }; -
✅ Use consistent naming conventions
interface UserProps { /* ... */ } // PascalCase for interfaces type UserId = string; // PascalCase for types -
✅ Keep interfaces focused and single-purpose
interface User { /* user data */ } interface Timestamped { createdAt: Date; updatedAt: Date; } -
✅ Use readonly for immutable properties
interface Config { readonly apiKey: string; } -
❌ Avoid mixing interface and type for same purpose
// ❌ Don't do this interface User { id: number; } type Admin = User & { role: string }; // ✅ Be consistent interface User { id: number; } interface Admin extends User { role: string; }
Next Steps#
- Learn about Classes and how to implement interfaces
- Explore Generics for reusable type-safe code
- Master Utility Types built into TypeScript