Understanding Interfaces in TypeScript
TypeScript interfaces define the shape of objects in your code. When working with types in TypeScript, interfaces help ensure consistent object structures. Type vs interface is an important consideration - interfaces are often preferred for object type definitions due to their extensibility.:
interface Person {
name: string;
age: number;
}
This interface outlines a Person
object with name
and age
properties.
Here is its practical usage:
const alice: Person = {
name: "Alice",
age: 30
};
Extending Interfaces in TypeScript
extends in TypeScript lets you build new interfaces from existing ones. This pattern promotes code reuse and clear hierarchies:
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
}
The Employee
interface now includes name
, age
, and employeeId
.
You can also extend interface with multiple interfaces:
interface HasEmail {
email: string;
}
interface Employee extends Person, HasEmail {
employeeId: number;
}
Using Interfaces with Classes in TypeScript
To use an interface with a class, apply the implements
keyword. The class must define all properties and methods specified in the interface. This pattern is especially useful when working with Convex server functions:
interface Car {
start(): void;
stop(): void;
}
class Tesla implements Car {
start(): void {
console.log('Starting the car');
}
stop(): void {
console.log('Stopping the car');
}
}
The Tesla
class meets the Car
interface by implementing the start
and stop
methods. A class can implement multiple interfaces. Here's how to combine function type declarations with interfaces in a more practical example:
interface Vehicle {
speed: number;
accelerate(amount: number): void;
}
interface Electric {
batteryLevel: number;
charge(): void;
}
class Tesla implements Vehicle, Electric {
speed = 0;
batteryLevel = 100;
accelerate(amount: number): void {
this.speed += amount;
}
charge(): void {
this.batteryLevel = 100;
}
}
Optional Properties in TypeScript Interfaces
The TypeScript type system lets you mark interface properties as optional using the optional chaining operator:
interface UserProfile {
userId: number;
bio?: string;
}
The bio
property is optional, allowing you to create a UserProfile
object with or without it:
const user1: UserProfile = { userId: 1, bio: 'Hello, world!' };
const user2: UserProfile = { userId: 2 };
Both user1
and user2
are valid UserProfile
objects.
You can also combine optional properties with type assertion when you need more precise control over property types. This is particularly useful when building complex filters in Convex:
interface Config {
baseUrl: string;
timeout?: number;
retries?: {
count: number;
delay: number;
};
}
const defaultConfig: Config = {
baseUrl: 'https://api.example.com',
timeout: 5000, // Optional but included
};
Readonly Properties in TypeScript Interfaces
The readonly modifier prevents properties from being changed after an object is created:
interface UserProfile {
readonly userId: number;
bio: string;
}
The userId
property can't be changed after the object is created:
const user: UserProfile = { userId: 1, bio: 'Hello, world!' };
user.userId = 2; // Error: Cannot assign to 'userId' because it is a read-only property.
When building applications with Convex, readonly properties help enforce immutability patterns in your data models:
interface Document {
readonly _id: string;
readonly _creationTime: number;
content: string;
}
Function Types in TypeScript Interfaces
TypeScript interfaces can define the shape of functions using function type syntax:
interface Calculator {
add(x: number, y: number): number;
subtract?(x: number, y: number): number; // Optional method
}
const calculator: Calculator = {
add(x: number, y: number): number {
return x + y;
}
};
The Calculator
interface specifies a function add
that takes two numbers and returns a number. It also defines an optional subtract
function that hasn't been implemented by the calculator
function.
Index Signatures in TypeScript Interfaces
index signature syntax lets you define interfaces for objects with dynamic property names using the [key: type]
syntax. For example:
interface StringMap {
[key: string]: string;
}
const map: StringMap = {
foo: 'bar',
baz: 'qux'
};
The StringMap
interface allows you to access properties using string keys.
To make these interfaces more flexible, you can combine them with utility types like Partial<T>
or Record<K, V>
:
type PartialDataRecord = `Partial<DataRecord>`;
type StringNumberRecord = `Record<string, number>`;
};
Common Challenges and Solutions
Extending Interfaces
When extending interfaces, you might run into naming conflicts:
interface Base {
data: string;
}
interface Feature {
data: number; // Conflict with Base.data
}
// Error: Interface 'Combined' cannot simultaneously extend types 'Base' and 'Feature'.
interface Combined extends Base, Feature {}
// Solution: Be explicit about property types in the extending interface
interface Better extends Base {
data: string; // Explicitly choose the type you want
numericData: number; // Add new property instead of conflicting
}
Implementing Interfaces with Classes
When implementing interfaces with optional properties, classes must handle undefined values:
interface UserConfig {
name: string;
theme?: string;
}
class UserSettings implements UserConfig {
name: string;
theme?: string;
constructor(name: string, theme?: string) {
this.name = name;
if (theme) {
this.theme = theme;
}
}
getTheme(): string {
return this.theme ?? 'default'; // Handle potentially undefined theme
}
}
Using Optional and Readonly Properties
When working with readonly properties, you need to plan for object updates:
interface User {
readonly id: string;
name: string;
}
function updateUser(user: User, newName: string): User {
// Don't modify the original due to readonly
return {
...user, // Spread the existing properties
name: newName // Update the mutable property
};
}
Final Thoughts
Interfaces help you write more maintainable TypeScript code by defining clear contracts between different parts of your application. They work especially well with code editors to catch type errors early and provide better autocompletion.