@fioc/core
FIoC (Fluid Inversion of Control) is a lightweight, reflection-free dependency injection (DI) library for TypeScript and JavaScript.
It simplifies dependency management with type safety, immutability, and a fluent builder API — designed for both frontend and backend projects.
FIoC powers the broader ecosystem, including integrations for React, Next.js.
💡 “Fluid” means your dependency graph is built fluently and safely — no decorators, no reflection metadata, no runtime hacks.
🚀 Quick Start
Install via npm, yarn, or pnpm:
npm install @fioc/core
yarn add @fioc/core
pnpm add @fioc/core
A minimal “Hello World” example (with inference comments):
import { buildDIContainer, createDIToken } from "@fioc/core";
interface Logger {
log(message: string): void;
}
const LoggerToken = createDIToken<Logger>().as("Logger");
const container = buildDIContainer()
.register(LoggerToken, { log: console.log })
.getResult();
const logger = container.resolve(LoggerToken);
logger.log("Hello, FIoC!");
✨ Features
- 🪶 Lightweight & Tree-Shakeable — zero reflection, minimal dependencies.
- 🎯 Type-Safe Resolution — no casting, all types inferred automatically.
- 🧱 Fluent Builder Pattern — chainable, immutable container configuration.
- 🔄 Immutable by Default — safe for concurrent or multithreaded use; supports scoped and singleton overrides.
- 🧬 Generic Type Metadata — Supports registering and resolving implementations based on interface tokens and their generic arguments (e.g.,
Repository<User>).
- 🔌 Universal — works in Node.js, browser, Deno, Bun, and serverless environments.
- 🧩 Flexible Factory System — register values, factories, or class constructors.
- ⚙️ Composable Containers — merge configurations or swap environments dynamically.
- 🔗 Ecosystem Foundation — powers:
-
@fioc/react
- @fioc/next
📘 Table of Contents
🪄 Creating Tokens
Tokens uniquely identify dependencies in the container.
import { createDIToken } from "@fioc/core";
interface ApiService {
getData: () => string;
}
const ApiServiceToken = createDIToken<ApiService>().as("ApiService");
or
import { DIToken } from "@fioc/core";
const ApiServiceToken: DIToken<ApiService> = "ApiService";
⚙️ Registering & Resolving
import { buildDIContainer } from "@fioc/core";
import { ApiServiceToken } from "./tokens";
const HttpApiService: ApiService = { getData: () => "Hello, World!" };
const container = buildDIContainer()
.register(ApiServiceToken, HttpApiService)
.getResult();
const api = container.resolve(ApiServiceToken);
api.getData();
Implementation note: FIoC containers are immutable; registering returns a new container builder result. You can merge containers by passing its states into a new container builder.
🏗️ Factories
Factories let you register logic that depends on other tokens.
Option 1 — Manual Configuration
Note: The object literal syntax is recommended if you need to keep your factories pure. It is recommended to use the fluent helper withDependencies (Option 2) below if you want less boilerplate.
const getDataUseCaseFactory = (apiService: ApiService) => () =>
apiService.getData();
const GetDataUseCaseToken =
createFactoryDIToken<typeof getDataUseCaseFactory>().as("GetDataUseCase");
const container = buildDIContainer()
.register(ApiServiceToken, HttpApiService)
.registerFactory(GetDataUseCaseToken, {
dependencies: [ApiServiceToken],
factory: getDataUseCaseFactory,
})
.getResult();
const useCase = container.resolve(GetDataUseCaseToken);
useCase();
Option 2 — With Dependencies Helper (Recommended: Clean & Strongly Typed)
import {
withDependencies,
createFactoryDIToken,
buildDIContainer,
} from "@fioc/core";
const getDataUseCaseFactory = withDependencies(ApiServiceToken).defineFactory(
(apiService ) => () => apiService.getData()
);
const GetDataUseCaseToken =
createFactoryDIToken<typeof getDataUseCaseFactory>().as("GetDataUseCase");
const container = buildDIContainer()
.register(ApiServiceToken, HttpApiService)
.registerFactory(GetDataUseCaseToken, getDataUseCaseFactory)
.getResult();
const useCase = container.resolve(GetDataUseCaseToken);
useCase();
🧱 Class Factories
import {
constructorToFactory,
withDependencies,
buildDIContainer,
createDIToken,
} from "@fioc/core";
class GetDataUseCase {
constructor(private apiService: ApiService) {}
execute = () => this.apiService.getData();
}
const GetDataUseCaseToken =
createDIToken<GetDataUseCase>().as("GetDataUseCase");
const container = buildDIContainer()
.register(ApiServiceToken, HttpApiService)
.registerFactory(
GetDataUseCaseToken,
withDependencies(ApiServiceToken).defineFactory(
constructorToFactory(GetDataUseCase)
)
)
.getResult();
const instance = container.resolve(GetDataUseCaseToken);
instance.execute();
🌀 Scopes
Clarified semantics and examples — with inference comments.
Definitions
transient (default): a new value/factory result is produced every time the token is resolved.
singleton: the first time the token is resolved in a given container, its value is created and then cached for all subsequent resolves on that container.
scoped: the token's value is cached per scope. Scopes are short-lived resolution contexts created from a container; each scope gets its own cache for scoped tokens.
Scoped example (Single Function for Sync/Async)
const container = buildDIContainer()
.registerFactory(MyToken, myFactory, "scoped")
.getResult();
let resolvedA;
let resolvedB;
container.createScope((scopedContainer) => {
resolvedA = scopedContainer.resolve(MyToken);
resolvedB = scopedContainer.resolve(MyToken);
resolvedA === resolvedB;
});
await container.createScope(async (scopedContainer) => {
await someAsyncOperation();
const resolvedC = scopedContainer.resolve(MyToken);
resolvedA === resolvedC;
});
When to use which
- Use
singleton for heavy or long-lived services (database connections, caches).
- Use
transient for stateless factories or values where fresh instances are required.
- Use
scoped for per-request or per-job resources that should be reused inside a single operation but isolated across operations.
🧬 Advanced: Generic Type Metadata
FIoC allows you to register tokens with metadata (implements and generics) to look up implementations of a generic interface at runtime. This mimics the functionality of runtime reflection without sacrificing tree-shakeability.
Example: Resolving a Repository by Generic Type
interface Repository<T> {
findOne(): T;
}
const RepositoryToken = createDIToken<Repository<any>>().as("Repository");
interface User {
id: number;
}
const UserToken = createDIToken<User>().as("User");
const UserRepositoryImpl: Repository<User> = { findOne: () => ({ id: 1 }) };
const UserRepositoryToken = createDIToken<typeof UserRepositoryImpl>().as(
"UserRepository",
{
implements: [RepositoryToken],
generics: [UserToken],
}
);
const container = buildDIContainer()
.register(UserRepositoryToken, UserRepositoryImpl)
.getResult();
const userRepos = container.resolveByMetadata(RepositoryToken, [UserToken]);
const user = userRepos[0].findOne();
findImplementationTokens | Returns a list of matching DITokens. |
resolveByMetadata | Returns a list of resolved instances of the matching tokens. |
🔀 Merge Containers
You can create isolated containers as modules and merge them together into a single container:
import { buildDIContainer } from "@fioc/core";
const containerA = buildDIContainer()
.register(ApiServiceToken, HttpApiService)
.getResult();
const containerB = buildDIContainer()
.register(ApiServiceToken, HttpApiService)
.getResult();
const container = buildDIContainer()
.merge(containerA.getState())
.merge(containerB.getState())
.getResult();
container.resolve(ApiServiceToken);
🧩 Container Manager
Switch between environments or test setups seamlessly:
import { buildDIManager } from "@fioc/core";
const manager = buildDIManager()
.registerContainer(productionContainer, "prod")
.registerContainer(testContainer, "test")
.getResult()
.setDefaultContainer(process.env.APP_ENV || "prod");
const container = manager.getContainer();
Use cases:
- Environment-specific containers
- Online/offline or mock/live switching
- Testing without global mutations
🧩 Why FIoC
Pros
- Reflection-free & decorator-free: Works without reflect-metadata, decorators, or runtime hacks → fully compatible with Deno, Bun, Node, and browsers.
- Tree-shakeable: Due to explicit dependency declaration and no reliance on reflection, only imported symbols are included in the final bundle → minimal footprint for frontend projects.
- Immutable container state: Safe for concurrent applications, serverless functions, and multi-threaded environments.
- Scoped lifecycles: Supports transient, singleton, and scoped instances with a single, reliable
createScope function.
- Generic Type Metadata: Unique ability to resolve dependencies based on generic type constraints, replacing a key feature of
reflect-metadata without the overhead.
- Strong TypeScript inference: Minimal boilerplate; dependencies are automatically type-checked and inferred.
- Fluent builder API: Chainable, readable syntax for container registration and composition.
- Modular & composable: Merge containers or swap configurations easily → ideal for testing or multi-environment setups.
Cons
- No automatic decorators: Users coming from decorator-based DI libraries may need to adjust patterns.
- Requires explicit token management: Every dependency needs a DIToken or factory token → slightly more verbose than reflection-based DI.
🌐 FIoC Ecosystem
The FIoC ecosystem provides specialized libraries for various environments:
@fioc/react: Hooks and context-based DI for React.
@fioc/next: Type-safe DI for Next.js Server Components and Actions.
🤝 Contributing
Contributions are welcome!
Feel free to open issues or submit pull requests on GitHub. Please include tests for behavioral changes and keep changes small and focused.
📜 License
Licensed under the MIT License.