Implementing Interfaces
The implements keyword is how a TypeScript class declares a contract: "I guarantee that I provide everything this interface requires." The compiler then verifies that guarantee at every class definition and every usage site.
Implementing interfaces is the foundation of programming to abstractions — code that depends on interfaces rather than concrete classes is more testable, modular, and extensible.
Basic Interface Implementation
An interface describes a shape — property names, types, and method signatures. A class that implements it must satisfy every member of that interface.
interface Printable {
print(): void;
}
interface Serializable {
serialize(): string;
deserialize(data: string): this;
}
class Document implements Printable, Serializable {
constructor(private content: string) {}
print(): void {
console.log(this.content);
}
serialize(): string {
return JSON.stringify({ content: this.content });
}
deserialize(data: string): this {
const parsed = JSON.parse(data) as { content: string };
return new (this.constructor as new (content: string) => this)(parsed.content);
}
}
const doc = new Document('Hello, World!');
doc.print(); // Hello, World!
const json = doc.serialize(); // '{"content":"Hello, World!"}'
const restored = doc.deserialize(json);
restored.print(); // Hello, World!What Implements Checks — and What It Doesn't
implements checks that the class has the right shape. It does NOT:
- Copy interface properties or methods into the class
- Affect runtime behaviour in any way
- Guarantee that the class correctly satisfies the interface's semantic contract (only the structural contract)
interface Resettable {
reset(): void;
readonly defaultValue: string;
}
class Counter implements Resettable {
defaultValue = 'zero'; // must be present — checked by implements
private count = 0;
reset(): void {
this.count = 0; // must have this method — checked
}
increment() {
this.count++;
}
// Classes can have MORE members than the interface requires:
decrement() { this.count--; }
get value() { return this.count; }
}
// Using the class as the interface type narrows what's visible:
const r: Resettable = new Counter();
r.reset(); // ✅ visible through interface
r.defaultValue; // ✅ visible through interface
// r.increment(); // ❌ Error — 'increment' is not on ResettableInterface vs implements: A Common Misconception
A common mistake is thinking implements means the class inherits from the interface. It does not. implements is purely a compile-time contract check.
interface Logger {
log(message: string): void;
warn(message: string): void;
error(message: string): void;
}
// ❌ WRONG — implements does NOT add these methods to the class
class ConsoleLogger implements Logger {
// If you forget to add these, you get a compile error
}
// Error: Class 'ConsoleLogger' incorrectly implements interface 'Logger'.
// Property 'log' is missing in type 'ConsoleLogger' but required in type 'Logger'.
// ✅ CORRECT — you must explicitly provide all interface members
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
warn(message: string): void {
console.warn(`[WARN] ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${message}`);
}
}Interfaces with Optional Members
Optional interface members (marked with ?) do not need to be implemented. If a class does implement them, they must match the correct type.
interface DataSource<T> {
fetch(id: string): Promise<T>;
fetchAll(): Promise<T[]>;
create?(item: Omit<T, 'id'>): Promise<T>; // optional
update?(id: string, patch: Partial<T>): Promise<T>; // optional
delete?(id: string): Promise<void>; // optional
}
// Minimal implementation — only required members
class ReadOnlyUserSource implements DataSource<User> {
async fetch(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async fetchAll(): Promise<User[]> {
const res = await fetch('/api/users');
return res.json();
}
// No create/update/delete — that's fine, they're optional
}
// Full implementation
class UserRepository implements DataSource<User> {
async fetch(id: string): Promise<User> { /* ... */ return {} as User; }
async fetchAll(): Promise<User[]> { /* ... */ return []; }
async create(data: Omit<User, 'id'>): Promise<User> { /* ... */ return {} as User; }
async update(id: string, patch: Partial<User>): Promise<User> { /* ... */ return {} as User; }
async delete(id: string): Promise<void> { /* ... */ }
}Implementing Generic Interfaces
Classes can implement generic interfaces, either keeping the type parameter generic themselves or fixing it to a concrete type.
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
save(item: T): Promise<T>;
remove(id: string): Promise<void>;
}
// Option A: Class is also generic (flexible, reusable)
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
private store = new Map<string, T>();
async findById(id: string): Promise<T | null> {
return this.store.get(id) ?? null;
}
async findAll(filter?: Partial<T>): Promise<T[]> {
const items = [...this.store.values()];
if (!filter) return items;
return items.filter(item =>
Object.entries(filter).every(([k, v]) => item[k as keyof T] === v)
);
}
async save(item: T): Promise<T> {
this.store.set(item.id, item);
return item;
}
async remove(id: string): Promise<void> {
this.store.delete(id);
}
}
// Option B: Class fixes the type parameter (concrete, single-purpose)
interface User { id: string; name: string; email: string }
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> { /* ... */ return null; }
async findAll(): Promise<User[]> { /* ... */ return []; }
async save(user: User): Promise<User> { /* ... */ return user; }
async remove(id: string): Promise<void> { /* ... */ }
}
// Usage
const userRepo = new InMemoryRepository<User>();
await userRepo.save({ id: '1', name: 'Alice', email: 'alice@example.com' });
const found = await userRepo.findById('1');
console.log(found?.name); // AliceInterface Segregation Principle
The Interface Segregation Principle (the I in SOLID) says: clients should not be forced to depend on methods they don't use. Compose small, focused interfaces rather than one large one.
// ❌ Fat interface — every implementor must provide everything
interface FileSystem {
read(path: string): Buffer;
write(path: string, data: Buffer): void;
delete(path: string): void;
createDirectory(path: string): void;
list(path: string): string[];
stat(path: string): FileStat;
watch(path: string, cb: () => void): Watcher;
}
// ✅ Segregated interfaces — implement only what you need
interface Readable { read(path: string): Buffer }
interface Writable { write(path: string, data: Buffer): void }
interface Deletable { delete(path: string): void }
interface Listable { list(path: string): string[] }
interface Watchable { watch(path: string, cb: () => void): Watcher }
// Full filesystem implementation
class LocalFileSystem implements Readable, Writable, Deletable, Listable, Watchable {
read(path: string): Buffer { return Buffer.alloc(0); }
write(path: string, data: Buffer): void {}
delete(path: string): void {}
list(path: string): string[] { return []; }
watch(path: string, cb: () => void): Watcher { return { stop() {} }; }
}
// In-memory test double — only what tests need
class MemoryFileSystem implements Readable, Writable {
private files = new Map<string, Buffer>();
read(path: string): Buffer { return this.files.get(path) ?? Buffer.alloc(0); }
write(path: string, data: Buffer): void { this.files.set(path, data); }
}
// A backup service only needs Readable
function backup(source: Readable, destination: Writable) {
const data = source.read('/data/backup.json');
destination.write('/backups/backup.json', data);
}Interfaces with Readonly Members
interface Coordinate {
readonly lat: number;
readonly lng: number;
distanceTo(other: Coordinate): number;
}
class GeoPoint implements Coordinate {
constructor(
public readonly lat: number,
public readonly lng: number,
) {}
distanceTo(other: Coordinate): number {
const R = 6371; // Earth radius in km
const dLat = ((other.lat - this.lat) * Math.PI) / 180;
const dLng = ((other.lng - this.lng) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((this.lat * Math.PI) / 180) *
Math.cos((other.lat * Math.PI) / 180) *
Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
}
const london = new GeoPoint(51.507, -0.128);
const paris = new GeoPoint(48.857, 2.352);
console.log(london.distanceTo(paris).toFixed(0)); // ~341 kmUsing Interfaces as Function Parameters
The real power of implements is that it lets you write functions and services that accept any class satisfying an interface — not just one specific class. This is dependency inversion.
interface EmailSender {
send(to: string, subject: string, body: string): Promise<void>;
}
// Production implementation
class SendGridEmailSender implements EmailSender {
constructor(private readonly apiKey: string) {}
async send(to: string, subject: string, body: string): Promise<void> {
await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to, subject, content: [{ type: 'text/html', value: body }] }),
});
}
}
// Test double — no real emails sent
class MockEmailSender implements EmailSender {
public sentEmails: Array<{ to: string; subject: string; body: string }> = [];
async send(to: string, subject: string, body: string): Promise<void> {
this.sentEmails.push({ to, subject, body });
}
}
// Service depends on the interface, not the concrete class
class AuthService {
constructor(private readonly emailSender: EmailSender) {}
async sendVerificationEmail(userEmail: string, token: string): Promise<void> {
const link = `https://app.example.com/verify?token=${token}`;
await this.emailSender.send(
userEmail,
'Verify your email',
`<a href="${link}">Click here to verify</a>`,
);
}
}
// Test — inject mock
const mock = new MockEmailSender();
const auth = new AuthService(mock);
await auth.sendVerificationEmail('user@example.com', 'abc123');
console.log(mock.sentEmails[0].subject); // 'Verify your email'
// Production — inject real sender
const real = new SendGridEmailSender(process.env.SENDGRID_API_KEY!);
const prodAuth = new AuthService(real);Summary
implements declares a structural contract — the compiler verifies the class provides every interface member.
A class can implement multiple interfaces separated by commas.
implements does NOT copy interface members into the class — you must write every method yourself.
Classes can have more members than the interface requires; only the required ones are checked.
Optional interface members (marked with ?) do not need to be implemented.
Implement generic interfaces by keeping the type parameter generic on the class or fixing it to a concrete type.
Prefer many small, focused interfaces (Interface Segregation Principle) over one large interface.
Depending on interfaces rather than concrete classes enables dependency inversion and easy testing.