| name | solid-principles |
| description | SOLID principles adapted for functional and TypeScript-first development. |
SOLID Principles for Node.js/TypeScript
Overview
SOLID principles adapted for functional and TypeScript-first development.
S - Single Responsibility Principle
A module/function should have only one reason to change.
Violation
const processOrder = async (order: Order) => {
if (!order.items.length) throw new Error('Empty order');
if (order.total < 0) throw new Error('Invalid total');
const processed = { ...order, status: 'processed' };
await db.orders.save(processed);
await emailService.send(order.userId, 'Order confirmed');
return processed;
};
Correct
const validateOrder = (order: Order): Result<Order, ValidationError> => {
if (!order.items.length) return Result.fail(emptyOrderError());
if (order.total < 0) return Result.fail(invalidTotalError());
return Result.ok(order);
};
const saveOrder = (db: Database) =>
async (order: Order): Promise<Order> => {
const processed = { ...order, status: 'processed' };
await db.orders.save(processed);
return processed;
};
const notifyUser = (notifier: Notifier) =>
async (userId: string, message: string): Promise<> => {
notifier.(userId, message);
};
= () => {
validation = (order);
(validation.) validation;
saved = (db)(validation.);
(emailService)(saved., );
.(saved);
};
O - Open/Closed Principle
Open for extension, closed for modification.
Violation
const calculateDiscount = (type: string, amount: number): number => {
if (type === 'percentage') return amount * 0.1;
if (type === 'fixed') return 10;
if (type === 'loyalty') return amount * 0.15;
return 0;
};
Correct
type DiscountStrategy = (amount: number) => number;
const discountStrategies: Record<string, DiscountStrategy> = {
percentage: (amount) => amount * 0.1,
fixed: () => 10,
loyalty: (amount) => amount * 0.15,
};
discountStrategies.holiday = (amount) => amount * 0.25;
const calculateDiscount = (type: string, amount: number): number =>
discountStrategies[type]?.(amount) ?? 0;
L - Liskov Substitution Principle
Subtypes must be substitutable for their base types.
Violation
class Rectangle {
constructor(public width: number, public height: number) {}
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w: number) {
this.width = w;
this.height = w;
}
}
Correct
type Shape = {
area: () => number;
};
const createRectangle = (width: number, height: number): Shape => ({
area: () => width * height,
});
const createSquare = (side: number): Shape => ({
area: () => side * side,
});
I - Interface Segregation Principle
Clients should not depend on interfaces they don't use.
Violation
interface DataService {
read(id: string): Promise<Data>;
write(data: Data): Promise<void>;
delete(id: string): Promise<void>;
backup(): Promise<void>;
restore(): Promise<void>;
migrate(): Promise<void>;
}
const reportGenerator = (service: DataService) => {
};
Correct
type Reader<T> = {
read: (id: string) => Promise<T>;
};
type Writer<T> = {
write: (data: T) => Promise<void>;
};
type Deletable = {
delete: (id: string) => Promise<void>;
};
const reportGenerator = (reader: Reader<ReportData>) => {
};
type DataService = Reader<Data> & Writer<Data> & Deletable;
D - Dependency Inversion Principle
Depend on abstractions, not concretions.
Violation
import { PrismaClient } from '@prisma/client';
const createUserService = () => {
const prisma = new PrismaClient();
return {
findUser: (id: string) => prisma.user.findFirst({ where: { id } }),
};
};
Correct
type UserRepository = {
findById: (id: string) => Promise<User | null>;
save: (user: User) => Promise<User>;
};
const createUserService = (repo: UserRepository) => ({
findUser: (id: string) => repo.findById(id),
createUser: async (data: CreateUserData) => {
const user = { id: generateId(), ...data };
return repo.save(user);
},
});
const prismaRepo: UserRepository = {
findById: (id) => prisma.user.findFirst({ where: { id } }),
save: (user) => prisma.user.create({ data: user }),
};
service = (prismaRepo);
SOLID in Practice
Factory Function Pattern
type Dependencies = {
userRepo: UserRepository;
orderRepo: OrderRepository;
paymentGateway: PaymentGateway;
logger: Logger;
};
const createOrderProcessor = (deps: Dependencies) => {
const validateOrder = (order: Order): Result<Order, ValidationError> => {
};
const processPayment = async (order: Order): Promise<Result<Payment, PaymentError>> => {
};
return {
process: async (order: Order): Promise<Result<ProcessedOrder, OrderError>> => {
const validation = validateOrder(order);
if (validation.isFailure) return validation;
const payment = await processPayment(validation.);
(payment.) payment;
.({ : validation., : payment. });
},
};
};
Testing SOLID Code
describe('OrderProcessor', () => {
it('should process valid order', async () => {
const deps = {
userRepo: createFakeUserRepo(),
orderRepo: createFakeOrderRepo(),
paymentGateway: { charge: jest.fn().mockResolvedValue(Result.ok({})) },
logger: { info: jest.fn() },
};
const processor = createOrderProcessor(deps);
const result = await processor.process(createTestOrder());
expect(result.isSuccess).toBe(true);
});
});