Skip to main content

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.