dipstick
v0.5.0
Published
A dependency injection framework for TypeScript
Readme
Dipstick is a dependency injection framework for TypeScript. Instead of using @Decorators, reflect-metadata, or unique strings to identify objects to the DI framework, Dipstick relies on the type system and code generation.
The result is a DI framework that works with the strengths of typescript, instead of hacking around it's weaknesses.
Simple.
Unlike other DI frameworks, which can sometimes feel like learning an entirely new language, learning Dipstick only requires understanding two concepts: Containers and Bindings.
Obvious.
The principle of least surprise is fundamental to Dipstick. There are no magic decorators or obtuse indirections in code. All of your IDEs tools like "Find references" and "go to symbol" work just as well with Dipstick as without. If you know typescript, understanding the code generated by Dipstick is a piece of cake.
Type Safe.
Dipstick works with the type system, instead of around it. Use the same Container types you declared to generate code in your integration tests to provide mocks for specific Containers. Don't worry about a DI framework spreading any throughout your codebase -- the types that come out of Dipstick are exactly as strong as you author them to be.
Installation
npm install dipstickOverview
Dipstick uses TypeScript's type system and code generation to create dependency injection containers. The framework supports both class instantiation and factory functions as implementations, making it flexible for various architectural patterns. The framework is designed to be type-safe and easy to use, with a focus on maintainability and developer experience.
Core Concepts
Containers
Containers are the core building blocks of Dipstick. They allow you to bind implementations to types that are used throughout your project. An instance of a container is akin to a "scope" in other DI frameworks -- the container instance will hold references to reusable bindings and static bindings. To create a container, export a type alias to Container, and define its bindings:
import { Container, Reusable, Transient } from 'dipstick';
interface IFoo {}
class Foo implements IFoo {}
interface IBaz {}
class Baz implements IBaz {}
function createBaz(foo: IFoo): IBaz {
return new Baz(foo);
}
// Export a type thats assignable to `Container` for dipstick to pick it up during code generation
export type MyContainer = Container<{
bindings: {
// Class bindings
foo: Reusable<Foo, IFoo>;
bar: Transient<Bar, IBar>;
// Function binding using typeof syntax
baz: Reusable<typeof createDatabase>;
};
}>;Bindings
Bindings allow containers to associate an implementation with a type. All bindings take two type arguments. The first argument can be either a class (which will be instantiated) or a function (using the typeof syntax). The second, optional argument is a type to return the instance as, such as an interface. Within a single container, no two bindings may return the same type alias.
export type MyContainer = Container<{
bindings: {
userIface: Transient<User, IUser>;
userImpl: Transient<User>;
};
}>;const container = new MyContainerImpl(); // MyContainerImpl is generated code
const userImpl = container.userImpl(); // User
const userIface = container.userIface(); // IUserUsing typeof with Factory Functions
When you use typeof functionName as the first type argument with only one argument, Dipstick automatically infers the bound type as ReturnType<typeof functionName>. This is particularly useful for factory functions:
// A factory function that returns a request handler
function createUserHandler(userService: IUserService) {
return (req: Request, res: Response) => {
const users = userService.getAll();
res.json(users);
};
}
export type HandlerContainer = Container<{
bindings: {
// Bound type is automatically ReturnType<typeof createUserHandler>
userHandler: Reusable<typeof createUserHandler>;
};
}>;Other containers can then depend on HandlerContainer and receive the handler type:
class App {
constructor(
// Type is ReturnType<typeof createUserHandler>
private readonly userHandler: ReturnType<typeof createUserHandler>
) {
this.app.get('/users', this.userHandler);
}
}
export type AppContainer = Container<{
bindings: {
app: Reusable<App>;
};
dependencies: [HandlerContainer];
}>;Dipstick will automatically match the ReturnType<typeof createUserHandler> parameter to the userHandler binding from HandlerContainer.
Bindings come in three flavors, which are described below.
Reusable Bindings
Reusable bindings return the same instance every time they are called. This is useful for singletons or other objects that should only be created once per container:
export type MyContainer = Container<{
bindings: {
// Returns the same Foo instance every time
foo: Reusable<Foo, IFoo>;
};
}>;Transient Bindings
Transient bindings return a new instance every time they are called. This is useful for objects that should be created fresh each time they are requested:
export type MyContainer = Container<{
bindings: {
// Returns a new Bar instance every time
bar: Transient<Bar>;
};
}>;Static Bindings
Static bindings are used to provide objects to a container when the container is instantiated. Use static bindings when you want to incorporate an object created outside of dipstick into a container so that it can be used as a dependency of other objects.
class RequestHandler {
constructor(req: Request, res: Response) {}
execute() {
res.send(200, `hello ${req.path}`);
}
}
export type RequestContainer = Container<{
bindings: {
// Created outside of this container
req: Static<Request>;
res: Static<Request>;
requestHandler: Transient<RequestHandler>;
};
}>;
app.use((req, res) => {
const container = new MyContainer({ req, res });
const handler = container.requestHandler();
handler.execute();
});Modularity & Composition
Containers can depend on other containers. These dependencies are used to resolve types that the container cannot resolve itself:
class Foo {
constructor(bar: Bar) {}
}
export type FooContainer = Container<{
bindings: {
foo: Reusable<Foo>;
};
}>;
export type BarContainer = Container<{
dependencies: [FooContainer];
bindings: {
bar: Transient<Bar>;
};
}>;
const fooContainer = new FooContainer();
const barContainer = new BarContainer([fooContainer]);
// if a container has both dependencies and static bindings, pass both:
// const barContainer = new BarContainer({ baz: new Baz() }, [ fooContainer ])Usage
- Define your containers using type aliases to
Container - Run the code generator:
npm exec -- dipstick generate ./path/to/tsconfig.json --verbose - Use the generated containers in your application:
const myContainer = new MyContainerImpl(); const service = myContainer.myService(); ...
Code Generation
The code generator will:
- Scan your TypeScript files for exported container type aliases
- Generate implementation classes for each container
- Handle dependency injection and binding resolution
- Ensure type safety throughout the dependency graph
Contributing
Contributions are welcome! Please see our CONTRIBUTORS.md guide for detailed information about:
- Setting up the development environment
- Running tests and code quality checks
- Code style guidelines
- Submitting pull requests
For quick contributions:
- Fork the repository
- Create a feature branch
- Make your changes
- Run
npm run build && npm run check && npm test - Submit a Pull Request

