npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typectx

v0.10.1

Published

Fully type-inferred Context and DI container for Typescript

Readme

typectx

Fully type-inferred Context and DI container for Typescript! Dependency injection (DI) and context propagation without reflect-metadata, decorators, annotations or compiler magic, just simple functions!

Why typectx?

  • Scalable architecture - Promotes SOLID, clean, and code-splittable design patterns.
  • Fully type-inferred - Compile-time dependency validation.
  • Minimal boilerplate - Trade-off: a bit more runtime boilerplate for much less type boilerplate.
  • Framework agnostic - Accommodates all codebases where TypeScript works
  • No magic - Just functions and closures, no classes, reflect-metadata, decorators, annotations or compiler magic.
  • Testing friendly - Easy mocking and dependency swapping
  • Performance focused - Smart memoization, lazy loading, code-splittable architecture.
  • Stateless - Dependencies resolved via closures, not global state.
  • A new DI paradigm - Don't let your past experiences with DI prevent you from trying this one!
  • Intuitive, opinionated terminology - The supply chain metaphor helps DI finally make intuitive sense.

Installation

npm install typectx

When to Use typectx

  • Complex TypeScript applications with deep function call hierarchies
  • Avoiding prop-drilling and waterfalls in React (works in both Client and Server Components)
    • Full alternative to React Context.
  • Microservices that need shared context propagation
  • Testing scenarios requiring easy mocking and dependency swapping
  • A/B testing, feature flagging and prototyping.
  • Any project wanting DI without the complexity of traditional containers

Performance

  • Hyper-minimalistic bundle size: ~5KB minified, ~2KB minzipped. Most of the package is type definitions.
  • Tree-shakable and code-splittable architecture: Helps you create hyper-specialized suppliers: One function or piece of data per supplier
  • Memory usage: Smart memoization prevents duplicate dependency resolution
  • Options to optimize dependency chain waterfalls: Define suppliers as lazy or eager, call init() to preload some values as soon as possible.

Quick Example

import { index, supplier } from "typectx"

// 1. Define request and product suppliers
const $session = supplier("session").request<{ userId: string }>()
const $todosDb = supplier("todosDb").product({
    suppliers: [],
    factory: () => new Map<string, string[]>() // Simple in-memory DB
})
const $addTodo = supplier("addTodo").product({
    suppliers: [$session, $todosDb],
    factory:
        ({ session, todosDb }) =>
        (todo: string) => {
            const userTodos = todosDb.get(session.userId) || []
            todosDb.set(session.userId, [...userTodos, todo])
            return todosDb.get(session.userId)
        }
})

const session = { userId: "user123" }

// 3. Assemble and use
const addTodo = $addTodo.assemble(index($session.pack(session))).unpack()

console.log(addTodo("Learn typectx")) // ["Learn typectx"]
console.log(addTodo("Build app")) // ["Learn typectx", "Build app"]

Intuitive, opinionated terminology

typectx uses an intuitive supply chain metaphor to make dependency injection easier to understand. You create fully-decoupled, hyper-specialized suppliers that exchange supplies in a free-market fashion to assemble new, more complex products.

| Term | Classical DI Equivalent | Description | | ----------------------------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | supplier(name).request() / supplier(name).product(config) | createContainer()+register() | Declare suppliers directly with explicit names and config. | | Supplier | Service | Provides dependencies to other suppliers. Node in your dependency graph. | | Request Supplier | Value Service | Supplier for a value from the user's request (request params, cookies, etc.) | | Product Supplier | Factory Service | Supplier for a value derived from other product or request suppliers via a factory function | | Supply or Pack | Proxy | Value wrapper for type-checking and transport across suppliers | | Supplies | Container / Context | The collection of resolved dependencies, but still within their supply or pack wrapper. | | assemble() | resolve() | Gathers all required request supplies (product supplies are auto-wired) and injects them in product supplier factories. | | Deps | Values | The collection of resolved unpacked dependencies a factory receives |

Full features list

☀️ General

  • Fully typesafe and type-inferred - Full TypeScript support with compile-time circular dependency detection.
  • Fluent and expressive API - Learn in minutes, designed for both developers and AI usage.
  • Fully framework-agnostic - Complements both back-end and front-end frameworks.

🔧 Dependency Injection

  • Functions only - No classes, decorators, annotations, or compiler magic.
  • Declarative, immutable, functionally pure.
  • Stateless - Dependencies are resolved via closures, not state. Some memoized state is kept for validation and optimization purposes only.
  • Auto-wired - All products are built by the Supply Chain and resolve their dependencies automatically.
  • Maximal colocation - All product suppliers are registered right next to the factory that uses them, not at the entry point.

📦 Context Propagation

  • Shared context - Assemble the context once at the entry point, access everywhere without prop-drilling.
  • Smart memoization - Dependencies injected once per assemble() context for optimal performance.
  • Context switching - Override context anywhere in the call stack with ctx($supplier).assemble(...).
  • Context enrichment - Add new context and products deep in the call stack with contextual assembly and hire(...).

⚡ Waterfall Management

  • Eager loading - Use lazy: false for immediate background construction of all supplies in parallel in assemble() call (default).
  • Lazy loading - Use lazy: true for on-demand call of the product supplier's factory when it's first accessed in another factory.

🧪 Testing and Packing

  • You can override any supplier with pack(), which uses the provided value directly and bypasses the supplier factory.
  • For more complex mocks which would benefit from a factory, see mock() below.

🚀 Mocking and A/B testing

  • Use mock() to create alternative implementations of a product supplier, that may depend on different suppliers than the original.
  • Mock factories must return products of the same type as the original product factory.
  • Define mock or alternative suppliers to hire() at the entry-point of your app
  • For example, you can easily hire different versions of a UI component for A/B testing.

Basic Usage

1. Declare suppliers directly

Suppliers are declared directly using supplier(name).request() and supplier(name).product(config). Names can only contain digits, letters, underscores or $ signs and cannot start with a digit, just like any Javascript identifier.

2. Define Request Suppliers

Request suppliers are used to wire the data you get from the user's request, and that cannot be derived from other request or product suppliers, like sessions or request params. You define a $request supplier and then .pack() it with a value at request time. The value can be anything you want, just specify its type.

// I like calling suppliers with $ prefix, but this is up to you.
const $session = supplier("session").request<{
    userId: string
}>()

const session = $session
    .pack({
        userId: "some-user-id"
    })
    .unpack()

3. Define Product Suppliers

Product suppliers are your application's services, components or features. They are factory functions that can depend on other products or request data. Factories can return anything: simple values, promises or other functions.

Dependencies are accessed via the 1st argument of the factory (deps).

const $user = supplier("user").product({
    suppliers: [$session, $db], // Depends on session and db suppliers.
    // properties of deps are the names provided to supplier("...").
    factory: ({ session, db }) => {
        return db.getUser(session.userId) // query the db to retrieve the user.
    }
})

Factory Lifecycle & Memoization

Important: Your factory function will only ever be called once per assemble() call. This eliminates the need for traditional DI service lifecycles (transient, scoped, singleton, etc.).

  • Need something called multiple times, or to run side-effects? Return a function from your factory instead of a value
// ✅ Good: Factory called once, returns a function for multiple calls or side-effects
const $createUser = supplier("createUser").product({
    suppliers: [$db],
    factory: ({ db }) => {
        // This setup code runs only once per assemble()
        const cache = new Map()

        // Return a function that can be called multiple times
        return (userId: string) => {
            if (cache.has(userId)) return cache.get(userId)
            const user = db.findUser(userId)
            cache.set(userId, user)
            return user
        }
    }
})

const createUser = $createUser.assemble(index($db.pack(db))).unpack()
// Usage: createUser() can be called multiple times
const user1 = createUser("123") // Fresh call
const user2 = createUser("123") // Cached result

Lazy vs Eager Loading

By default, all products are constructed on assemble() call immediately in the background and in parallel, no matter how deep in the supply chain. This means no waterfalls happen. You can disable this for specific products with lazy: true.

// Eager loading - constructed immediately when assemble() is called
const $eagerService = supplier("eagerService").product({
    suppliers: [$db],
    factory: ({ db }) => buildExpensiveService(db)
    lazy: false // default
})

// Lazy loading - constructed only when accessed
const $lazyService = supplier("lazyService").product({
    suppliers: [$db],
    factory: ({db}) => buildExpensiveService(db),
    lazy: true // Loaded when first accessed via deps
})

4. Define Your Application

Your Application is just a product like the other ones. It's the main, most complex product at the top of the supply chain.

const $app = supplier("app").product({
    suppliers: [$user], // Depends on User product
    // Destructure the user value.
    // Its type will be automatically inferred from $user's factory's inferred return type.
    factory: ({ user }) => {
        return <h1>Hello, {user.name}! </h1>
    }
})

5. Assemble at Entry Point

At your application's entry point, you assemble your main $app, providing just the request data (not the products) requested recursively by the $app's suppliers chain. Typescript will tell you if any request data is missing.


const req = //...Get the current http request

// Assemble the App, providing the Session and Db resources.
// Bad but working syntax for demonstration purposes only. See index() below for syntactic sugar.
const app = $app.assemble({
    [$session.name]: $session.pack({
        userId: req.userId
    }),
    [$params.name]: params.pack(req.params)
}).unpack()

// Return or render app...

The flow of the assemble call is as follows: request data is obtained, which is provided to $request suppliers using pack(). Then those request data are supplied to $app's suppliers recursively, which assemble their own product, and pass them up along the supply chain until they reach $app, which assembles the final app product. All this work happens in the background, no matter the complexity of your application.

To simplify the assemble() call, you should use the index() utility, which just transforms an array like ...[req1, product1] into an indexed object like {[req1.name]: req1, [product1.name]: product1}. I unfortunately did not find a way to merge index() with assemble() without losing assemble's type-safety, because typescript doesn't have an unordered tuple type.

import { index } from "typectx"

const app = $app
    .assemble(
        index(
            $session.pack({
                userId: req.userId
            }),
            $db.pack(db)
        )
    )
    .unpack()

Optionals

Sometimes a product can work with or without certain dependencies. For these cases, use the optionals parameter alongside suppliers. Optional values may be undefined at runtime, and TypeScript will enforce proper undefined checks. You can also use optionals if a piece of request data is not yet known at the entry point of the application, so you don't want Typescript to enforce it being supplied in the assemble() call.

Learn more about optionals →

Context Propagation

Context propagation is typectx's flagship capability. It lets you reassemble products deeper in the call stack by using ctx($supplier).assemble(...) with additional or overridden request supplies. This creates nested sub-contexts without global state and keeps dependency resolution type-safe end to end.

Learn more about context propagation →

Testing and Packing

1. Mocking in tests with .pack()

You usually use pack() to provide request data to assemble(), but you can also use pack() on products. This allows to provide a value for that product directly, bypassing its factory. Perfect to override a product's implementation with a mock for testing.

const $profile = supplier("profile").product({
    suppliers: [$user],
    factory: ({ user }) => {
        return <h1>Profile of {user.name}</h1>
    }
})

const $user = supplier("user").product({
    suppliers: [$db, $session],
    factory: ({db,session}) => {
        return db.findUserById(session.userId)
    }
})

//Test the profile
const profile = $profile.assemble(
    index(
        //$user's factory will not be called, but...
        $user.pack({ name: "John Doe" })
         //assemble still requires a valid value for db and session when using pack(),
         // since $db and $session are in the supply chain...
        $db.pack(undefined),
        // if you can't pass undefined, or some mock for them,
        // prefer using `.mock()` and `.hire()` instead.
        $session.pack(undefined),
    )
).unpack()

// profile === <h1>Profile of John Doe</h1>

2. .mock() and .hire() alternative implementations

For more complete alternative implementations, with complex dependency needs, you can use .mock() and .hire() instead of .pack() to access the whole power of your supply chain. The same example as above could be:

const $profile = supplier("profile").product({
    suppliers: [$user],
    factory: ({ user }) => {
        return <h1>Profile of {user.name}</h1>
    }
})

const $user = supplier("user").product({
    suppliers: [$db, $session],
    factory: ({ db, session }) => {
        return db.findUserById(session.userId)
    }
})

const $userMock = $user.mock({
    suppliers: [],
    factory: () => "John Doe"
})

//You no longer need to pass some value for $db and $session, since $userMock removes them from the supply chain.
const profile = $profile.hire($userMock).assemble({}).unpack()

profile === <h1>Profile of John Doe</h1>

.mock() and .hire() can be used for testing, but also to swap implementations at runtime for sandboxing or A/B testing.

Design Philosophy: The Problem with Traditional DI

DI containers have always felt abstract, technical, almost magical in how they work. Like a black box, you often have to dig into the source code of a third-party library to understand how data flows in your own application. It feels like you lose control of your own data when you use one, and your entire app becomes dependent on the container to even work. typectx aims to make DI cool again! The pattern has real power, even if current implementations on the open-source market hide that power under a lot of complexity.

DI was complex to achieve in pure OOP world because of the absence of first-class functions. But in more functional languages, DI should be easier, since DI itself is a functional pattern. However, TypeScript DI frameworks currently available seem to have been built by imitating how they were built in OOP languages...

The problem DI was solving in OOP still exists in a more functional world. In OOP, DI helped inject data and services freely within deeply nested class hierarchies and architectures. With only functions though, DI achieves a similar purpose: inject data and services freely in deeply nested function calls. Deeply nested function calls naturally emerge when trying to decouple and implement SOLID principles in medium to highly complex applications. Without DI, you cannot achieve maximal decoupling. Even if in principle you can reuse a function elsewhere, the function is still bound in some way to the particular call stack in which it finds itself, simply by the fact that it can only be called from a parent function that has access to all the data and dependencies it needs.

typectx's Context containers can do everything DI containers do, and more! It also achieves it in a more elegant, simpler, and easier-to-reason-about manner.

How it Works Under the Hood

Injection happens statelessly via a memoized, recursive, self-referential, lazy object. Here is a simplified example:

const supplies = {
    // request data is provided directly
    reqA,
    reqB,

    // Products are wrapped in a function to be lazily evaluated and memoized.
    // The supplies object is passed to assemble, creating a recursive structure.
    productA: once(() => productA.supplier.assemble(supplies)),
    productB: once(() => productB.supplier.assemble(supplies))
    // ...
}

The assemble() call builds the above supplies object, each product now ready to be injected and built right away if eager, or on-demand if lazy.

This functional approach allows typescript to follow the types across the entirety of the dependency chain, which it cannot do for traditional stateful containers.

API reference

See the docs for the full API reference!

Contributing

We welcome contributions! Please feel free to submit issues and pull requests.

License

MIT