Coder Social home page Coder Social logo

molszanski / iti Goto Github PK

View Code? Open in Web Editor NEW
129.0 2.0 6.0 2.03 MB

~1kB Dependency Injection Library for Typescript and React with a unique support of async flow

Home Page: https://itijs.org

License: MIT License

TypeScript 87.25% JavaScript 10.33% CSS 2.37% HTML 0.06%
dependency-inversion ioc ioc-container di dependency-injection type-safety typescript react

iti's Introduction

ITI Logo

Iti

~1kB Dependency Injection Library for Typescript and React with a unique async flow support

CI Status npm version gzip Coverage Mutation Score

  • supports async(!): merges async code and constructor injection via plain async functions
  • non-invasive: does not require imported @decorators or framework extends in your application business logic
  • strongly typed: has great IDE autocomplete and compile time check. Without any manual type casting
  • lazy: initializes your app modules and containers on demand
  • split chunks: enables dynamic imports via a one liner thanks to a fully async core
  • React friendly: has useful React bindings to help you separate application business logic and a React view layer
  • starter friendly: works with starters like Create React App or Next.js unlike existing libraries
  • no Babel config: doesn't require reflect-metadata or decorators so there is no need to hack in decorator and "decoratorMetadata" support in to your build configs
  • tiny: less than 1kB

IoC is an amazing pattern and it should easy to adopt, fully support async and without hard to learn APIs or complex tooling requirements.

Iti relies on plain JS functions, objects and familiar patterns. API is simple so you can make a proof of concept integration in minutes.

It is an alternative to InversifyJS and microsoft/tsyringe for constructor injection.

At Packhelp we’ve refactored most of our 65K SLOC Editor app, that didn't have any IoC, to Iti in under 5 hours

Usage

// kitchen.ts
export class Oven {
  public pizzasInOven() {
    return 7
  }
  public async preheat() {}
}
export class Kitchen {
  constructor(public oven: Oven, public userManual: string) {}
}
// Application code is free of framework dependencies of decorators
// app.ts
import { createContainer } from "iti"
import { Oven, Kitchen } from "./kitchen"

const container = createContainer()
  .add({
    key: () => new Item(),
    oven: () => new Oven(),
    userManual: async () => "Please preheat before use",
  })
  .add((items) => ({
    kitchen: async () => new Kitchen(items.oven, await items.userManual),
  }))

await container.get("kitchen") // Kitchen
// MyPizzaComponent.tsx
export const PizzaData = () => {
  const kitchen = useContainer().kitchen
  return <>Pizzas In Oven: {kitchen.oven.pizzasInOven()}</>
}

Why another library?

The main reason is that existing libraries don’t support asynchronous code. Iti brings hassle free and fully typed way to use async code.

Secondly, existing libraries rely on decorators and reflect-metadata[^1]. They couple your application business logic with a single framework and they tend to become unnecessarily complex. Also existing implementations will likely be incompatible with a TC39 proposal.

Also it is hard to use reflect-metadata with starters like CRA, Next.js etc. You need to eject or hack starters and it is far from ideal.

Short Manual

Reading

// Get a single instance
container.get("oven") // Creates a new Oven instance
container.get("oven") // Gets a cached Oven instance

await container.get("kitchen") // { kitchen: Kitchen } also cached
await container.items.kitchen // same as above

// Get multiple instances at once
await container.getContainerSet(["oven", "userManual"]) // { userManual: '...', oven: Oven }
await container.getContainerSet((c) => [c.userManual, c.oven]) // same as above

// Plain deletion
container.delete("kitchen")

// Subscribe to container changes
container.subscribeToContainer("oven", (oven) => {})
container.subscribeToContainerSet(
  ["oven", "kitchen"],
  ({ oven, kitchen }) => {},
)
// prettier-ignore
container.subscribeToContainerSet((c) => [c.kitchen], ({ oven, kitchen }) => {})
container.on("containerUpdated", ({ key, newItem }) => {})
container.on("containerUpserted", ({ key, newItem }) => {})
container.on("containerDeleted", ({ key, newItem }) => {})

// Disposing
container
  .add({ dbConnection: () => connectToDb(process.env.dbUrl) })
  .addDisposer({ dbConnection: (db) => db.disconnect() }) // waits for promise
await container.dispose("dbConnection")
await container.disposeAll()

Writing

let container = createContainer()
  .add({
    userManual: "Please preheat before use",
    oven: () => new Oven(),
  })
  .upsert((items, cont) => ({
    userManual: "Works better when hot",
    preheatedOven: async () => {
      await items.oven.preheat()
      return items.oven
    },
  }))

// `add` is typesafe and a runtime safe method. Hence we've used `upsert`
try {
  container.add({
    // @ts-expect-error
    userManual: "You shall not pass",
    // Type Error: (property) userManual: "You are overwriting this token. It is not safe. Use an unsafe `upsert` method"
  })
} catch (err) {
  err.message // Error Tokens already exist: ['userManual']
}

Patterns and tips

Lifecycle

Single Instance (a.k.a. Singleton)

let cont = createContainer().add({
  oven: () => new Oven(),
})
cont.get("oven") === cont.get("oven") // true

Transient

let cont = createContainer().add({
  oven: () => () => new Oven(),
})
cont.get("oven") === cont.get("oven") // false

Dynamic Imports

// ./kitchen/index.ts
export async function provideKitchenContainer() {
  const { Kitchen } = await import("./kitchen/kitchen")
  return {
    kitchen: () => new Kitchen(),
    oven: async () => {
      const { Oven } = await import("./kitchen/oven")
      const oven = new Oven()
      await oven.preheat()
      return oven
    },
  }
}
// ./index.ts
import { createContainer } from "iti"
import { provideKitchenContainer } from "./kitchen"
let cont = createContainer().add({
  kitchen: async () => provideKitchenContainer(),
})

// Next line will load `./kitchen/kitchen` module
await cont.items.kitchen

// Next line will load `./kitchen/oven` module
await cont.items.kitchen.oven

Getting Started

The best way to get started is to check a CRA Pizza example

Typescript

Iti has a great typescript support. All types are resolved automatically and checked at compile time.

Autocomplete Autocomplete Autocomplete Autocomplete

Docs

Read more at itijs.org/docs/api

Notable inspiration

iti's People

Contributors

molszanski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

iti's Issues

Type resolution fails when importing iti with "moduleResolution": "NodeNext"

Problem Description

When using iti in a TypeScript project with "moduleResolution": "NodeNext" in your tsconfig.json, type information for iti cannot be located.

This line:

import { createContainer } from 'iti'

Produces the error:

Could not find a declaration file for module 'iti'. '<project path>/node_modules/iti/dist/iti.modern.js'
implicitly has an 'any' type.
  Try `npm i --save-dev @types/iti` if it exists or add a new declaration (.d.ts) file
  containing `declare module 'iti';`

The project will still compile and run with tsc; you just don't have type information for iti for static checking, linting, etc.

Cause

There are two causes:

  1. The exports map in iti's package.json contains an exports map, but the types key is not defined for each export. See this TypeScript issue for a complete explanation. When exports is defined, top-level types or typings keys are ignored.

  2. The export ... from statements in iti/src/index.ts need to use an explicit .js extension. Without this, import { createContainer } from 'iti' will nominally succeed, but the type of createContainer will just be import, and the static analysis gets very confused.

Proposed fix

  1. The first issue is simple: just add "types": "./dist/src/index.d.ts" to the exports map in package.json.

  2. For the second issue, I'll be honest: I'm fairly new to JS/TS. My understanding is that explicit .js extensions are required for import/export statements when using ESM ("type": "module" in package.json). So I'm not sure how iti is getting away with "bare" imports in the first place. But in any case, adding extensions only to the export statements in src/index.ts is enough to satisfy the consumer's linter and should be backwards-compatible with CJS. With that said, two of the iti tests (dispose....) try to import { createContainer } from '../src', which needs to be ../src/iti after this change. The other tests already import this way.

I have a fork with these proposed changes made to iti and iti-react. I can make a PR if you'd like.

feature: container disposing

A useful feature is to be able to dispose of resources from the container.

E.g. disconnect from the databases and so on.

See how Awilix did it.

Subset of features for faster type inference

Is your feature request related to a problem? Please describe.
Adding a lot of add(..) slows down Intellisense to the point that I have to restart the TS server every now and then.

Describe the solution you'd like
I was wondering whether it would be feasible to provide a version of iti that has less features but provides faster type inference. I haven't dug into the code yet, just wondering whether something like that is out of the question.

What would be an ideal API for your use case?
I just need add(), upsert() and items(). No async injection or any of that disposal API.

Improve type inference on `getContainerSet`

Is your feature request related to a problem? Please describe.

Considering the following code.

const a = await root.getContainerSet(["userManual", "oven"])
a.oven // There is no error, oven exists, the type in compile-time is the same that the type in run-time

const b = await root.getContainerSet(() => ["userManual"])
b.oven // There is no error, BUT oven DOES NOT exist, the type in compile-time is NOT the same that the type in run-time

It is a problem in this example:

const kitchenContainer = async ({ oven, userManual }) => {
  await oven.preheat()
  return {
    kitchen: new Kitchen(oven, userManual),
  }
}

const node = root.add((ctx, node) => ({
  kitchen: async () =>
    kitchenContainer(await node.getContainerSet(["userManual"])),
}))

We are requiring from kitchenContainer two dependencies: over and userManual. But we are only providing userManual. This will cause an error at run-time.

Describe the solution you'd like

The returned type of getContainerSet should be automatically inferred from what we pass in parameter.

Solution

(tested and it works)

Edit these lines

iti/iti/src/iti.ts

Lines 371 to 373 in 0a3a006

public async getContainerSet<T extends keyof Context>(
tokensOrCb: KeysOrCb<Context>,
) {
to

 public async getContainerSet<T extends keyof Context>( 
   tokensOrCb: Pick<KeysOrCb<Context>, T>, 
 ) { 

(This solution may be applicable to other functions like subscribeToContainerSet and _extractTokens too)

Note: We can improve the type printing (on hovering on VSCode for example), by wrapping the Pick<...> into the Prettify helper.

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};
With PrettifyWithout Prettify
Capture d’écran 2024-04-06 à 02 54 27 Capture d’écran 2024-04-06 à 02 54 44

Event Hooks for dependencies

Class-based / object dependencies could implement event hooks.

For example

class Logger implements Init {
     init() {
       this.info("logger started");
     }
}

or

{ init : () => this.info("logger started") }

There could be various hooks on dependencies that would be called when an iti even occurs.
Describe the solution you'd like
A clear and concise description of what you want to happen.
Naive solution
For any / all events, loop through dependencies, calling corresponding function.

//init event occurs
for(dependency of Object.values(cache)) {
   dependency?.init?.()
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.