JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q38471F
  • License MIT

Fluent, type-safe IoC container for JS/TS, simplifying dependency management in React, Next.js, and Node.js projects

Package Exports

  • @fioc/core

Readme

@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, and stricter compile-time validation with @fioc/strict.

πŸ’‘ β€œ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
# or
yarn add @fioc/core
# or
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");
// LoggerToken: DIToken<Logger, "Logger">

const container = buildDIContainer()
  .register(LoggerToken, { log: console.log })
  .getResult();

// Resolve β€” inferred return type is Logger
const logger = container.resolve(LoggerToken); // logger: Logger
logger.log("Hello, FIoC!");

✨ Features

  • πŸͺΆ Lightweight β€” zero reflection, minimal dependencies (only depends on Immer).
  • 🎯 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.
  • πŸ”Œ 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:

πŸ“˜ 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");
// ApiServiceToken: DIToken<ApiService, "ApiService">

Alternatively, you can use a manually casted Symbol (not compatible with @fioc/strict):

import { DIToken } from "@fioc/core";

const ApiServiceToken: DIToken<ApiService> = Symbol.for("ApiService");
// ApiServiceToken: DIToken<ApiService>

βš™οΈ Registering & Resolving

import { buildDIContainer } from "@fioc/core";
import { ApiServiceToken } from "./tokens";

const HttpApiService: ApiService = { getData: () => "Hello, World!" };

const container = buildDIContainer()
  .register(ApiServiceToken, HttpApiService) // registers ApiService
  .getResult();

const api = container.resolve(ApiServiceToken); // api: ApiService
api.getData(); // "Hello, World!"

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 (with inference comments)

import { createFactoryDIToken, buildDIContainer } from "@fioc/core";
import { ApiServiceToken } from "./tokens";

const getDataUseCaseFactory = (apiService: ApiService) => () =>
  apiService.getData();

const GetDataUseCaseToken =
  createFactoryDIToken<typeof getDataUseCaseFactory>().as("GetDataUseCase");
// GetDataUseCaseToken: FactoryDIToken<() => string, "GetDataUseCase">

const container = buildDIContainer()
  .register(ApiServiceToken, HttpApiService)
  .registerFactory(GetDataUseCaseToken, {
    dependencies: [ApiServiceToken], // Will get type error if doesn't match types and orders of factory's parameters
    factory: getDataUseCaseFactory,
  })
  .getResult();

const useCase = container.resolve(GetDataUseCaseToken); // useCase: () => string
useCase();

Option 2 β€” With Dependencies Helper (clean & strongly typed)

import {
  withDependencies,
  createFactoryDIToken,
  buildDIContainer,
} from "@fioc/core";

const getDataUseCaseFactory = withDependencies(ApiServiceToken).defineFactory(
  (apiService /* inferred as ApiService */) => () => apiService.getData()
);

const GetDataUseCaseToken =
  createFactoryDIToken<typeof getDataUseCaseFactory>().as("GetDataUseCase");

const container = buildDIContainer()
  .register(ApiServiceToken, HttpApiService)
  .registerFactory(GetDataUseCaseToken, getDataUseCaseFactory);

const useCase = container.resolve(GetDataUseCaseToken); // useCase: () => string
useCase();

🧱 Class Factories

import {
  constructorToFactory,
  buildDIContainer,
  createDIToken,
} from "@fioc/core";

class GetDataUseCase {
  constructor(private apiService: ApiService) {}
  execute = () => this.apiService.getData();
}

const GetDataUseCaseToken =
  createDIToken<GetDataUseCase>().as("GetDataUseCase");
// GetDataUseCaseToken: DIToken<GetDataUseCase, "GetDataUseCase">

const container = buildDIContainer()
  .register(ApiServiceToken, HttpApiService)
  .registerFactory(GetDataUseCaseToken, {
    dependencies: [ApiServiceToken],
    factory: constructorToFactory(GetDataUseCase),
  });

// resolve and inferred type is GetDataUseCase
const instance = container.resolve(GetDataUseCaseToken); // instance: GetDataUseCase
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.

Implementation note: FIoC containers are immutable; registering returns a new container builder result. Scopes are lightweight resolution contexts that reuse container registration metadata but keep separate caches for scoped instances.

Singleton example

const container = buildDIContainer()
  .registerFactory(MyToken, myFactory, "singleton")
  .getResult();

const a = container.resolve(MyToken); // a: Inferred type of MyToken
const b = container.resolve(MyToken); // same cached instance
a === b; // true

Transient example

const container = buildDIContainer()
  .registerFactory(MyToken, myFactory, "transient") // or omit scope (default)
  .getResult();

const a = container.resolve(MyToken); // new instance/value
const b = container.resolve(MyToken); // another new instance/value
a === b; // false

Scoped example (callback-style)

const container = buildDIContainer()
  .registerFactory(MyToken, myFactory, "scoped")
  .getResult();

let resolvedA: ReturnType<typeof container.resolve>;
let resolvedB: ReturnType<typeof container.resolve>;

container.createScope((resolve) => {
  // `resolve` has the same inference as container.resolve
  resolvedA = resolve(MyToken); // resolvedA: inferred type
  resolvedB = resolve(MyToken); // cached inside this scope
  resolvedA === resolvedB; // true (same scope)
});

// different scope -> different instance
container.createScope((resolve) => {
  const resolvedC = resolve(MyToken);
  resolvedA === resolvedC; // false
});

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.

πŸ”€ 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); // HttpApiService

🧩 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.

  • Immutable container state: Safe for concurrent applications, serverless functions, and multi-threaded environments.

  • Scoped lifecycles: Supports transient, singleton, and scoped instances β†’ flexible per-request, per-job, or long-lived resources.

  • 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.

  • Tree-shakeable: Only imported symbols are included in the final bundle β†’ minimal footprint for frontend projects.

  • Ecosystem ready: Integrates with React (@fioc/react), Next.js (@fioc/next), and stricter type-checking (@fioc/strict).

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/strict: Enhanced type safety and compile-time validation.
  • @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.