Dependency Injection in TypeScript: For Beginners
\n\nIn the world of software development, the term Dependency Injection (DI) is often encountered. For a beginner TypeScript developer, this may sound intimidating, but in reality it is a simple and powerful pattern that makes your code cleaner, more testable, and easier to maintain. In this article, we will break down what DI is, why it is needed, and how to apply it in TypeScript in practice.
\n\nWhat is Dependency Injection?
\n\nImagine you are assembling a computer. Instead of soldering the processor directly onto the motherboard at the factory, you leave a slot into which you can insert any compatible processor. Dependency Injection works on the same principle: instead of a class creating its own dependencies (e.g., a database or HTTP client), it receives them from the outside.
\n\nIn simple terms, DI is when an object does not create its "helpers" inside itself, but receives them ready-made through a constructor or method.
\n\nThe Problem Without DI: Tight Coupling
\n\nLet's look at an example often found in real projects:
\n\nclass UserService {\n private database: MySQLDatabase;\n\n constructor() {\n this.database = new MySQLDatabase(); // Tight coupling!\n }\n\n getUser(id: number) {\n return this.database.findUserById(id);\n }\n}\n\nHere, UserService is tightly coupled to MySQLDatabase. If you want to switch to PostgreSQL or write unit tests with a fake database, you will have to rewrite the entire class. This violates the Open-Closed principle from SOLID.
The Solution with DI: Flexibility and Testability
\n\nNow let's rewrite the same class using DI:
\n\ninterface Database {\n findUserById(id: number): User | null;\n}\n\nclass UserService {\n constructor(private database: Database) {} // Dependency is passed from outside\n\n getUser(id: number) {\n return this.database.findUserById(id);\n }\n}\n\n// Usage\nconst mySQL = new MySQLDatabase();\nconst service = new UserService(mySQL); // Injecting the dependency\n\nNow UserService doesn't know which database it is working with. You can easily replace MySQLDatabase with PostgreSQLDatabase or MockDatabase for testing.
Main Ways to Inject Dependencies
\n\nIn TypeScript, there are three main ways to inject dependencies. Let's look at each of them.
\n\n1. Constructor Injection
\n\nThis is the most common and recommended method. Dependencies are passed through the class constructor.
\n\nclass Logger {\n log(message: string) {\n console.log(`[LOG]: ${message}`);\n }\n}\n\nclass OrderService {\n constructor(private logger: Logger) {}\n\n createOrder(data: any) {\n this.logger.log('Order created');\n // order creation logic\n }\n}\n\nconst logger = new Logger();\nconst orderService = new OrderService(logger);\n\n2. Property Injection
\n\nThe dependency is set directly into a property of the object after its creation. It is used less frequently as it can lead to incomplete object initialization.
\n\nclass EmailService {\n sendEmail(to: string, message: string) {\n console.log(`Email sent to ${to}: ${message}`);\n }\n}\n\nclass NotificationService {\n emailService!: EmailService; // Type assertion\n\n notifyUser(user: string) {\n this.emailService.sendEmail(user, 'Hello!');\n }\n}\n\nconst notification = new NotificationService();\nnotification.emailService = new EmailService(); // Setting the property\nnotification.notifyUser('user@example.com');\n\n