Dependency Injection is a design pattern used in software engineering that helps to decouple the various components of an application by allowing objects to be created with their dependencies supplied from external sources rather than being created within the object itself.

In other words, Dependency Injection (DI) is a technique in which an object receives its dependencies from an external source, rather than creating them itself.

Here's a working code example of dependency injection in Node.js using TypeScript:

// Define an interface for the database class
interface IDatabase {
  save(data: any): Promise<void>;
}

// Define a class that implements the IDatabase interface
class Database implements IDatabase {
  async save(data: any) {
    // Save the data to the database
    console.log('Data saved to the database:', data);
  }
}

// Define a repository interface for the User model
interface IUserRepository {
  save(user: any): Promise<void>;
}

// Define a class that implements the IUserRepository interface
class UserRepository implements IUserRepository {
  constructor(private database: IDatabase) {}
  async save(user: any) {
    // Use the injected database instance to save the user
    console.log('Saving user to the database...');
    await this.database.save(user);
    console.log('User saved to the database:', user);
  }
}

// Define a User model class that has a dependency on the UserRepository
class User {
  constructor(private userRepository: IUserRepository) {}
  async save(newUser: any) {
    // Use the injected userRepository instance to save the user
    console.log('Creating a new user...');
    await this.userRepository.save(newUser);
    console.log('New user created:', newUser);
  }
}

// Create instances of the classes
const database = new Database();
const userRepository = new UserRepository(database);
const user = new User(userRepository);


// Use the User instance to create a user
const newUser = { name: 'John Doe', email: 'johndoe@example.com' };
user.save(newUser).catch((error) => {
  console.error(error);
});

In this example, we define an interface IDatabase that defines a save method. We then create a Database class that implements this interface. Next, we define an interface IUserRepository for the User model and create a UserRepository class that implements this interface, taking an instance of IDatabase as a parameter in the constructor.

We then define a User class that has a dependency on the IUserRepository interface. The save method of the User class takes a newUser object as a parameter and uses the injected userRepository instance to save the user.

Finally, we create instances of the classes, passing the necessary dependencies through the constructors. We then use the User instance to create a user, passing in the newUser object. The output of the program will be:

Creating a new user...
Saving user to the database...
Data saved to the database: { name: 'John Doe', email: 'johndoe@example.com' }
User saved to the database: { name: 'John Doe', email: 'johndoe@example.com' }
New user created: { name: 'John Doe', email: 'johndoe@example.com' }

This demonstrates how dependency injection allows us to inject dependencies into our classes at runtime, making it easy to change dependencies or mock them out during testing.