Giter Club home page Giter Club logo

typedi's Introduction

Elegant dependency injection in TypeScript and JavaScript.


Contributors wanted! For a place to start, check out the list of good first issues or open an issue to talk.

Warning

From 0.4.0, support for Node versions under v15.3.0 has been removed, as the package has transitioned to ES Modules. Support for CommonJS has been removed.

See Why does the package not support Node versions under v15.3.0?

Note

Experimental support for Bun has also been introduced. Feel free to test and report any issues!

Features

  • Optional, SkipSelf, Self, Many and more, just like Angular!
  • 100% functional injection, without any runtime reflection (no more reflect-metadata!)
  • 10kB bundle size1 (3.8k gzip'd2), and no dependencies3.
  • Simplify testing across your entire app, with a simple suite of injection tools to get you started4.
  • Rigorously tested, type-safe API5.

Get Started

To start, you'll want to install the NPM package as part of your project.

$ npm install @freshgum/typedi

After that, you should be good to go. Read the documentation to get started!

Quick Example

import { Service, Container, Token } from '@freshgum/typedi';

// Make a service that logs a message to the console.
type LogFunction = (...args: any[]) => void;
const LOG_FUNCTION = new Token<LogFunction>();

@Service([LOG_FUNCTION])
class LogService {
  constructor(private logFn: LogFunction) {}

  log(message: string) {
    this.logFn(message);
  }
}

// Then, use our logging service in our root service,
// which will log "hello world!" to the console.
@Service([LogService])
class RootService {
  // Store the instance of the logger inside the class.
  constructor(private logger: LogService) {}
  run() {
    this.logger.log('hello world!');
  }
}

// Set the logging function...
Container.set(LOG_FUNCTION, console.log);

// Now, run our service!
Container.get(RootService).run();

Runtime Support

This DI implementation doesn't require any sort of precompilation / bundler steps, aside from the usual one provided by your TypeScript compiler.

Project Goals

Some goals of the project include:

  1. To keep things simple: Containers are essentially type-safe Map instances, with smarts for resolving dependencies, managing container hierarchies, resolving constraints, and some additional functionality.
  2. Avoid reinventing the wheel: I could bundle an endless amount of code in this package, but I'd rather not -- instead, let's allow this project to focus on one thing and execute it well, as opposed to doing hundreds of things badly.
  3. Have a minimal presence in bundles: As it stands, the production builds of this project are around 10kB. Other libraries tend to be much larger, which typically leads to a lot of unused functionality and slower loading times on constrained network connections.
  4. Extensibility: The container, and all other parts of the package, should be extendable by the consumer, instead of forcing everyone into a pre-defined workflow. However, steps are taken to ensure that the consumer is guided into making The Right Decision(tm) when not doing so would be harder to manage.
  5. To allow for opaque implementations: The package should avoid using global state where possible (aside from the default container, of course), to prevent libraries using the package from conflicting with end-user code.
  6. Avoid breaking end-user code: As the package is sub-v1, this isn't as much of a priority right now; however, very few breaking changes are introduced, and the changes that are made are typically made for good reasons (and are documented in the changelog.). Unnecessarily breaking end-user code is pointless, frustrating, and causes work for everyone.
  7. Make it easy to create well-defined, stable interfaces: Currently, this is done through simply using service classes. Instances of said classes can then be attained and used as regular instances.
  8. Avoid magical syntax, in favour of easy, simple syntax: Avoid using "magic"6 when doing so would be unnecessary, and would obfuscate what the code does to anyone not familiar with this container implementation.
  9. Encompass isomorphism.: Some other DI implementations access Node-specific APIs (e.g. the filesystem); we don't want to do that here. Instead of tying the library to a specific runtime, or set of libraries, this implementation aims to be compatible across different bundlers, ecosystems, runners, and more. However, we can't explicitly guarantee compatibility with runtimes we don't personally use; if you think we've missed something, however, please open an issue.
  10. Do one thing, and do it well. Regarding filesystems, we've specifically made a note of avoiding doing anything like that here; it's fragile, hard to debug, and generally becomes an annoyance as a project scales, files are moved, and paths have to be continually updated. Instead of that, regular import statements are used; this dramatically simplifies any required refactoring work, and general maintenance of consumer code.

This container was initially made for my own use in code, though I will happily take suggestions, issues and feature requests to improve it.

Project Non-Goals

These will be added if any features are requested that are not compatible with the project goals.

"Why was this created?"

It's mainly a more modern, simpler version of the original TypeDI project, with more features, easier integration, and better error reporting. The naming isn't ideal, and it'll most likely be changed in the future7.

Maintenance

Yes. I regularly use it as part of my packages (and so do others!)8. I didn't put in all this effort just to abandon the project. However, bear in mind that, as the goal of this package is to do one thing well, there may not be updates for periods if they are not explicitly required9, or if the addition of further functionality would go against the project goals.

However, I will happily review any MRs made against the source tree. If you wish to suggest a feature, I would prefer it if you could open an issue first to discuss whether the feature is appropriate, and whether its implementation is feasible.

License

Released under MIT by @freshgum. Forked from typestack's implementation, therefore this project contains code published by upstream TypeDI contributors.

Footnotes

  1. Tested on 23/11/2023. A lot of work is made to reduce the size of the bundle (a lot of work has also been inlined into other, non-related commits). Note that bundle size tests are performed by copying the minified typedi.min.mjs file into ByteSizeMatters -- there are most likely better ways to test this. Investigation on reducing bundle size is then performed by formatting the minified file with Prettier, and assessing the bundle for unnecessary code / possible refactors; this is done iteratively until I am unable to find any further code size optimizations (which would not negatively affect performance / result in breaking changes). An example of a trick used to reduce the bundle size is name mangling: the Rollup configuration file contains code to minify certain members of internal classes (such as VisitorCollection).

  2. Tested via pnpm run build; cd build/bundles; for file in *.mjs; do printf "$file\t$(cat $file | wc -c | xargs printf "%'d\n")\t$(gzip -9c $file | wc -c | xargs printf "%'d\n")\t$(brotli -cZ $file | wc -c | xargs printf "%'d\n")\n"; done | tee (credit: mrienstra on Stack Overflow)

  3. No runtime dependencies are included. The only dependency of this package is type-fest (which only provides TypeScript types which are used internally). This dependency has been version-locked to ensure any breaches of that package's security does not impact anyone using this package. Any updates are checked and verified to ensure they do not contain malicious code.

  4. This mainly refers to the package's standard container-based interface, which makes testing easy (as you can replace services and values at any time). Further work is being done on a more featureful testing suite, which would be able to simplify the overall testing process.

  5. I haven't counted each one, but I'd say that the package exports ~40 types (as of writing: 23/11/2023); a lot of the safety is provided through typing, as opposed to unnecessary runtime checks, which affect performance and code size.

  6. An example of "magic", in this context, would be integration with the filesystem to read a configuration file in a proprietary format, and then using that to configure services -- while that might make more sense for Java developers, such features don't (in my experience) scale well in JavaScript. Also, we'd have to write a ton of editor integrations! </ramble>

  7. In the future, I'll most likely look at renaming this package. That'll come naturally as part of a wider project. You'll probably notice that I avoid explicitly using this package's name in a lot of places; that will make it easier to update. The naming scheme is... unfortunate, and in retrospect I should have named it differently to avoid confusion with the original project.

  8. One example of such a project is ListenBrainz Discord RPC, which makes use of this package to structure its functionality into modular services. There are some other examples on GitHub's Dependents view, too.

  9. If the library is ever feature-complete, it'll still be maintained -- compatibility with the latest engines will still be tested. However, as stated prior, features would not be added for the sake of adding features. Therefore, if this package ever becomes feature-complete (and is placed into maintenance mode), there's no need to ask if it's abandoned. If I ever become unable to continue maintaining the package, it shall be placed into archival (and the NPM package will become deprecated); in that case, please fork it and continue my efforts. All power to you!

typedi's People

Contributors

2betop avatar asvetliakov avatar attilaorosz avatar baumandm avatar bbakhrom avatar binki avatar bruno-brant avatar bsitruk avatar codemilli avatar dependabot[bot] avatar digitalkaoz avatar freshgum-bubbles avatar github-actions[bot] avatar gjdass avatar happyzombies avatar klassm avatar nightink avatar nonameprovided avatar orimdominic avatar pepakriz avatar petermetz avatar pleerock avatar sobolevn avatar suchcodemuchwow avatar timwolla avatar tonivj5 avatar zibanpirate 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

Watchers

 avatar  avatar  avatar

typedi's Issues

Create a way for services to get the container

After removing code that passes in the container as a constructor parameter, there's no way for services to actually get and interact with the container.
While the Service Locator pattern is generally discouraged, an escape hatch should be provided for code which still requires it.

This could be achieved by the addition of a function which returns an ID, similar to how the resolution constraints API was implemented.

A check for said ID would then be added to ContainerInstance.get, wherein it would immediately return itself. A new Token and a .set call seems harder to implement, especially with container inheritance (what if they specifically requested a local identifier?)

Ideas

@Service([
  HostContainer()
])
export class MyService {
  constructor (private container: ContainerInstance) { }
}

Notes: If implemented like above, this will work with SkipSelf() to allow the consumer to neatly resolve the parent container. (It should also work with other constraints, though Self() and Many() would obviously be pointless.)

Dependencies aren't currently type-checked

Currently, the parameters of a class aren't type-checked against its dependencies.

Consider the following:

@Service([AnotherService])
export class MyService {
  constructor (private randomValue: number) { }
}

It's a clear error, but it still compiles.
This is because currently, TypeDI does not read the constructor's arguments.
Instead, the arguments are typed as any[].

I believe I've found a workaround for this; a PR can be expected soon.

Confusion regarding arguments passed to service factory tuples

Describe the bug
factory function with arguments,It's not the engine.
To Reproduce

import {Container, Service} from '@freshgum/typedi';

@Service([])
class Engine {
    public type = 'V8';

    public get name() : string {
        return 'bmw';
    }
}

@Service([Engine])
class CarFactory {
    createCar(engine: Engine) {
        console.log(engine);    // ContainerInstance not Engine
        engine.type = 'V6';
        return new Car(engine);
    }
}

@Service({ factory: [CarFactory, 'createCar'] }, [Engine])
class Car {
    constructor(public engine: Engine) {

    }
}

const car = Container.get(Car);
console.log(car.engine.type);
console.log(car.engine.name);  // undefined
  • OS: MacOS
  • Browser Chrome
  • Version ^0.7.1

Container not registering classes defined in external directories

Hi, im having trouble trying to get the dependency injection to work if my files are in different directories

Im using the following structure for my project

usecases/
infrastructure/
entities/
web/

where web is a next js app
infrastructure is where I defined the following classes

import { Service } from '@freshgum/typedi';
import IAuthService from "../../../usecases/common/interfaces/authService";
import type IGateway from "../interfaces/gateway";
import ApiGateway from '../gateways/gateway';


@Service({ scope: "transient" }, [ApiGateway])
export default class AuthService implements IAuthService {
    private readonly gateway : IGateway;

    /**
     *
     */
    constructor(gateway : IGateway) {
        this.gateway = gateway;
    }
}
import IGateway from "../interfaces/gateway";
import { Service } from "@freshgum/typedi";

@Service({ scope: "singleton" }, [])
export default class ApiGateway implements IGateway {
    ...
}

EDIT: I try to use this command inside a page of my next's project

if I try to use Container.get(AuthService);

I get the following error

ServiceNotFoundError: Service with "MaybeConstructable<AuthService>" identifier was not found in the container. Register it before usage via "Container.set" or the "@Service()" decorator.

its the same error if I try to register it manually

let identifier : ServiceIdentifier = AuthService;
if (!Container.has(identifier)) {
    identifier = Container.set(AuthService);
}
const service = Container.get(AuthService);

these are my dependencies in package.json file in /web

"dependencies": {
    "18": "^0.0.0",
    "@freshgum/typedi": "^0.6.0",
    "@headlessui/react": "^1.7.17",
    "@heroicons/react": "^2.0.18",
    "@tremor/react": "^3.6.6",
    "@types/react": "18.2.20",
    "@types/react-dom": "18.2.7",
    "@types/react-router-dom": "^5.3.3",
    "axios": "^1.5.1",
    "next": "13.5.6",
    "next-auth": "^4.23.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-hook-form": "^7.48.2",
    "react-router-dom": "^6.17.0",
    "reflect-metadata": "^0.1.13",
    "sharp": "^0.32.6",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20",
    "autoprefixer": "^10",
    "eslint": "^8",
    "eslint-config-next": "13.5.4",
    "postcss": "^8",
    "tailwindcss": "^3",
    "ts-loader": "^9.5.0",
    "typescript": "^5"
  }

and this is the next.config.js file

/** @type {import('next').NextConfig} */
const nextConfig = {
    images: {
        domains: ['avatars.githubusercontent.com', 'avatar.vercel.sh']
      },
    experimental: {
      serverActions: true,
      externalDir: true,
    },
    output: 'standalone',
}

module.exports = nextConfig

it works fine if dependencies are inside a directory, as I managed to write a test file in the infrastructure folder to try injecting them (although it only works with classes defined inside the infrastructure folder)

any advise on how can I make it work using multiple directories? thanks.

CommonJS Support

Is your question related to a contrib/ feature?
No

Question
Is this library suppose to work with CommonJS modules at all? I have a Typescript project that builds to CommonJS and I'm having trouble to make it work with TypeDI++. I see that in node_modules a cjs directory is present, but I'm still confused.

Create Documentation

The main goal of this project is to create a better TypeDI. Also, one that I and others can use in projects.
Without documentation, that's quite hard to do.

This is definitely a goal for 1.0.

ESLint Switch-On

The ESLint configuration was a bit pedantic for my liking, so I turned it off.
I'd like to turn ESLint back on, perhaps with less telling-offs, sometime in future.

Support typescript stage 3 decorator ?

I'm going to use stage 3 decorator in my typescript project with inversify, but can't not use stage 2 and stage 3 decorator at the same time.Any plan to support stage 3 decorator?

Clean up GitHub Actions jobs

Currently the GitHub Actions CI workflow is... pretty ugly.
We copy-paste the Node+PNPM installer pretty much everywhere.

To attempt to fix this, I tried to make a reusable workflow which didn't go so well.

I'd really appreciate help from someone who actually knows how to use GitHub Actions.

- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: 'lts/*'
registry-url: https://registry.npmjs.org
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8.6.1
- name: Set PNPM Cache Directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Restore PNPM Package Cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

Package probably doesn't work in Bun

The package.json's exports property lists "bun": "./src/index.mts", but that path doesn't exist.

I'm getting really tired of figuring out the package.json is fucked in production.
There must be some sort of linter that'd catch this, right?

Container Plug-Ins

Dagger has a concept called Service Provider Interface, which allows consumers to hook into the module loading process.

Some use-cases of this include:

  1. Generating a report of the overall service tree
  2. Adding warnings for services which should be scoped to a container (instead of being global)
  3. Throwing errors when the DI container or any of its services are used incorrectly

I believe this would be a valuable addition to the project,
although I'm not entirely sure how it would be implemented.
Dagger's concept of loading modules differs entirely from that of TypeDI and,
therefore, the implementation of this in TypeDI will most likely be very different.

Another point of note is that, currently, there's no other way to hook into the default container.

As of now, you could hack this together with the following:

export class HookedContainerInstance extends ContainerInstance {
  get (x, y) { /* ... */ }
}

const hookedDefaultContainer = new HookedContainerInstance('default');
await ContainerRegistry.removeContainer(ContainerInstance.defaultContainer);
ContainerRegistry.registerContainer(hookedDefaultContainer);

However, this is very error-prone. None of the services have been copied over,
meaning providers declared via @Service and with reference to the default
container will no longer be resolvable.
Furthermore, it relies on awaiting the removal of the container.
This may introduce race conditions and result in undefined behaviour.


This is food for thought; this is a great feature and something I'd like to implement.

Release on NPM

At some point, this should be released on NPM so I can actually use it.

Per-version changelogs

Now we're approaching 1.0, it would be nice to have changelogs for each push to stable.
These could be generated from commit names, or done manually (or a combination of both!).

Regression in unreleased 0.6.0's Container dispose functinality

Describe the bug

In 0.6.0, the ContainerInstance.dispose method now correctly awaits the disposing of said container.
Unfortunately, this seems to have introduced regressions in the test suite, which are still being investigated.

0.6.0 has not yet been released, so there's a window to fix these unexpected regressions before they go live.

.js.org subdomain

It would be nice to have a domain ready for when docs hit stable.
js.org might be a good contendor,
as they provide subdomains of .js.org to organisations managing NPM packages etc.

Alternatively, we could stick with the standard github.io subdomain.

Explicit Resource Management

Currently, the API is rather unsafe, in that a service could be disposed as another service is using it.
We could combat this using an explicit resource management framework, wherein services are attained
via references, and those references can be released later. This provides guarantees to the calling code
that a service won't dispose itself while it's in-use.

We could accomplish the above using a sort of garbage collector, with a simplified implementation of
the reference counting algorithm, which is a fairly universal concept.

The advantage of this algorithm is that it gives us more information as to how the caller is using the provided
symbol, so we can make more informed decisions later.

The current APIs should not be affected by this change. In the case of a reference counter, Calls to get
should not be reference-counted to ensure backwards compatibility. Additionally, we should ensure that the
usual behaviour of disposing services (after a .get call) is still satisfied.

Find a basic implementation of this below:

const reference = Container.refer(MyService);
const myService = reference.current;

// This won't do anything, as a reference to MyService is currently held:
Container.remove(MyService);

await myService.runAsyncCode();
reference.release();

// The identifier is now removed.

A way to use this in services could be:

@Service([
  Refer(MyService)
])
class AnotherService {
  constructor (private myServiceRef: Refer<MyService>) { }
}

Questions

  • In the case of Container.remove, it would probably be best to immediately remove the identifier from the map, to ensure compatibility with code that would expect the older behaviour.
  • One aspect of interest is calling reset when a reference is held: when that reference is released, should the identifier be immediately discarded? I personally think this would be wise.

Provide a way to get fresh transient services without HostContainer

Currently, the only way to get fresh instances of a transient service are by using HostContainer.
It feels nasty to recommend using what is essentially an escape hatch instead of a more sensible API.

import { Service, HostContainer } from '@typed-inject/inject';
import { TimerService } from './timer.service';

@Service([
    HostContainer()
])
export class PageService {
    constructor (private container: ContainerInstance) { }

    async renderPage () {
        const timer = this.container.get(TimerService);
        timer.setName('page-render');
        timer.start();
        // Perform page rendering logic...
        timer.end();
    }
}

Types marked with @internal are excluded from typings, but still imported

Latest contains the following in container-instance:

import { ServiceOptions, ServiceOptionsWithDependencies, ServiceOptionsWithoutTypeOrDependencies } from './interfaces/service-options.interface.mjs';

...but ServiceOptionsWithoutTypeOrDependencies is marked as @internal in the TSDoc.
This seems more like a TypeScript bug, tbh.

A way to subscribe to container creation

It might be helpful if a way to subscribe to the creation of a certain container was implemented.
This is similar to CustomElementsRegistry.whenDefined.

Needs investigation as to whether this feature has any merit and is feasible.

For instance:

Container.whenDefined('task-1', container => console.log(container.get('CONFIG')));

Implementation Challenges

  • Should the listener immediately be called when the container is created, or when its received all services?
  • If 2, how do we know when the services have all been added?

Misuse of resolution constraints is not checked

Currently, the following is allowed:

import { Service, Container, HostContainer, SkipSelf() } from '@typed-inject/injector';

const myContainer = Container.ofChild(Symbol());
const childContainer = Container.ofChild(Symbol());

@Service({ container: childContainer }, [
  [HostContainer(), SkipSelf() | SkipSelf()]
])
const LogService {
  constructor (private container: ContainerInstance) {
    // Due to the doubled usage of SkipSelf(),
    // the consumer may expect it to traverse up *twice*.
    // In that case, the result would be the default container.
    // However, this isn't the case, so the following is true:
    assert(container === myContainer);
  }
}

childContainer.get(LogService);

I feel like this should be documented somewhere.

Built-ins can be set in containers

Built-ins such as Number and String can currently be set
to values in the Container. This is a problem, as the API
never resolves built-ins to container values.

Therefore, we should prevent this by checking if a value
passed to Container.set is a built-in before we add it.

Unable to build node.js typescript project using the library: Missing exported members

Describe the bug
Project does not build, due to typedi build errors:

> [email protected] build
> tsc -p tsconfig.json                   

node_modules/@freshgum/typedi/build/esm5/container-instance.class.d.mts:4:58 - error TS2724: '"./interfaces/service-options.interface.mjs"' has no exported member named 'ServiceOptionsWithoutTypeOrDependencies'. Did you mean 'ServiceOptionsWithoutDependencies'?
                                                                                                                                                                                                                                                                     
4 import { ServiceOptions, ServiceOptionsWithDependencies, ServiceOptionsWithoutTypeOrDependencies } from './interfaces/service-options.interface.mjs';                                                                                                              
                                                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                                                                                                                                   

node_modules/@freshgum/typedi/build/esm5/container-instance.class.d.mts:12:10 - error TS2305: Module '"./types/service-identifier-location.type.mjs"' has no exported member 'ServiceIdentifierLocation'.

12 import { ServiceIdentifierLocation } from './types/service-identifier-location.type.mjs';
            ~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/@freshgum/typedi/build/esm5/functions/lazy.function.d.mts:2:10 - error TS2305: Module '"../types/infer-service-type.type.mjs"' has no exported member 'InferServiceType'.

2 import { InferServiceType } from '../types/infer-service-type.type.mjs';
           ~~~~~~~~~~~~~~~~

node_modules/@freshgum/typedi/build/esm5/index.d.mts:21:10 - error TS2305: Module '"./types/service-identifier-location.type.mjs"' has no exported member 'ServiceIdentifierLocation'.

21 export { ServiceIdentifierLocation } from './types/service-identifier-location.type.mjs';
            ~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/@freshgum/typedi/build/esm5/interfaces/tree-visitor.interface.d.mts:4:10 - error TS2305: Module '"../types/service-identifier-location.type.mjs"' has no exported member 'ServiceIdentifierLocation'.

4 import { ServiceIdentifierLocation } from '../types/service-identifier-location.type.mjs';
           ~~~~~~~~~~~~~~~~~~~~~~~~~

node_modules/@freshgum/typedi/build/esm5/types/type-wrapper.type.d.mts:3:10 - error TS2305: Module '"./infer-service-type.type.mjs"' has no exported member 'InferServiceType'.

3 import { InferServiceType } from './infer-service-type.type.mjs';
           ~~~~~~~~~~~~~~~~


Found 6 errors in 5 files.

Errors  Files
     2  node_modules/@freshgum/typedi/build/esm5/container-instance.class.d.mts:4
     1  node_modules/@freshgum/typedi/build/esm5/functions/lazy.function.d.mts:2
     1  node_modules/@freshgum/typedi/build/esm5/index.d.mts:21
     1  node_modules/@freshgum/typedi/build/esm5/interfaces/tree-visitor.interface.d.mts:4
     1  node_modules/@freshgum/typedi/build/esm5/types/type-wrapper.type.d.mts:3

To Reproduce

Use preconfigured project:

  1. Clone https://github.com/JonathanIlk/typedi-build-issue
  2. npm run build

DIY:

  1. Download typescript node.js boilerplate project (https://github.com/jsynowiec/node-typescript-boilerplate)
  2. Add @freshgum/typedi as dependency to package.json
  3. Add experimentalDecorators to tsconfig.json
  4. Add class with @service Decorator to main.ts
  5. npm run build

Expected behavior
Project builds correctly.

Desktop (please complete the following information):

  • OS: Windows
  • Typescript: 5.1

Additional context
The missing types appear to be missing from the interface files. However they seem to be available in the source code in the github.
Maybe they went missing when deploying the package to npm?

Add a (better) way to notify services of container disposal

Currently, containers are just dropped from the service with no warning:

public reset(options: { strategy: 'resetValue' | 'resetServices' } = { strategy: 'resetValue' }): this {
this.throwIfDisposed();
switch (options.strategy) {
case 'resetValue':
this.metadataMap.forEach(service => this.disposeServiceInstance(service));
break;
case 'resetServices':
this.metadataMap.forEach(service => this.disposeServiceInstance(service));
this.metadataMap.clear();
this.multiServiceIds.clear();
break;
default:
throw new Error('Received invalid reset strategy.');
}
return this;
}

This seems quite strange. Services would surely want some form of notification
as to when their container is disposed of.

While a seemingly simple concept, the notion of how individual services are meant
to respond to such a notification is ambiguous. For instance, in the case of a database
service, is it meant to close down its database connection(s) when the container is disposed?
Furthermore, in such a case, should we provide a way for the caller of .dispose or .reset to
know when all such asynchronous events have been concluded?

One further challenge is how you would provide an API for such a notification.
It would be awkward to make services conform to a standard interface for notification,
and any runtime type-checking of notification handler methods could incur runtime errors.

This somewhat ties into typestack/typedi#230:

allow containers to dispose of themselves, destroying all locally registered service

Proposed APIs

Addition of @DisposeService decorator

@Service([])
export class DatabaseService {
  @DisposeService
  async dispose(): Promise<void> { }
}

@Service([DatabaseService])
export class RootService {
  constructor (private database: DatabaseService) { }

  @DisposeService([DatabaseService])
  async dispose (): Promise<void> { }
}

The decorator allows a list of dependencies to be passed.
The dependencies are guaranteed not to be disposed before
the returned dispose promise is settled.

For example, in the case of RootService, it may want to
perform further operations on the database (flushing caches, etc.)
before the database is disposed of (which would close the connection).
In this case, it can declare that it requires DatabaseService to dispose of
the application correctly. As such, it can return a Promise that, until settled,
will have an undisposed reference to DatabaseService available.

Use HostContainer

This is already a no-go, but for posterity...

Using this API would mean that tests would have to pass in a container instance for potentially nothing.
Also, we'd have to expose an addDisposeListener or the likes onto the class, which feels awkward.


Definitely needs further investigation.

Version 1 (Stable) Roadmap

I'd like to get this library stable some time soon.

There are a few "nice to haves" that'd be, well, nice to have, before that happens.
Think of this as a quasi-block but, if enough time passes, these features won't make
it into this roadmap window.

typestack/typedi compatibility layer proposal

Compatibility layer with typestack/typedi

#158 essentially proposes a compatibility layer which allows the usage of ++ in places where typestack's implementation is expected.

I definitely think this is worth exploring; it's not a use-case I originally considered,
but I do think it would expand the horizons of where ++ could be used.

A wise investment would be creating a list of incompatibilities with source.
Let's do that here.

  • xxx

General compatibility with typestack

Compatibility with typestack has been a bit of a bug bear of mine.
I've been meaning to make it easier to migrate to this one.
I can only imagine the work required to have to migrate at-once; this could definitely be made easier.

Development Mode Ideas

Potentially, a development mode could be introduced into TypeDI++.
This would assist with the introspection of individual containers, as well as a few other features.

As a baseline for whether it's worth implementing this in the first place, I'll create a list here and
add scenarios to it wherein the usage of development mode could be advantageous.

  • warning when a service is changed after services depending upon that service have already been constructed
  • [ ]

API Reference not present at typedi.js.org

Hi! I switched to this fork from upstream and I'm finding some of the APIs confusing, but that's beside the point. I went to the docs, and every doc link worked except for the API Reference. /api-reference returns a 404, in fact. Maybe it's related to this build error I get when i build the API reference using the typedoc command? I've run out of things to check on my end so I don't know whether it's an issue with this repo or not.

 ~/c/u/typedi  on develop  pnpm install && pnpm run docs:api-reference
Lockfile is up to date, resolution step is skipped
Packages: +772
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 0, reused 716, downloaded 0, added 772, done

> @freshgum/[email protected] prepare /home/blue/code/utils/typedi
> husky install

husky - Git hooks installed
Done in 8s

> @freshgum/[email protected] docs:api-reference /home/blue/code/utils/typedi
> rimraf website/build/api-reference && typedoc

[info] Loaded plugin @mxssfd/typedoc-theme
[warning] The entrypoint glob ./src/index.ts did not match any files.
[error] Unable to find any entry points. See previous warnings.
 ELIFECYCLE  Command failed with exit code 3.

 ~/c/u/typedi  on develop   

PNPM version: 8.7.0
Node version v18.17.0
typedi version: latest commit as of 10/03/23 10:26 EDT
(also tried with Bun package manager / runtime 1.0.3, didn't work either)

If there's a better way to read the API docs or compile them myself, please do let me know!

  • blue

EDIT: sorry for not clarifying, but i did try and fix all of the issues, like first changing the entrypoint, then setting the typedoc tsconfig to use the tsconfig.spec, however none of them ultimately solved the problems and it still threw dozens of compile errors relating to JSX usage and various unresolveable modules.

Asynchronous Services: A Better Solution

In one of my applications, I have quite a few services which require explicit bootstrapping
before they can be safely used. In the case of that specific application, my solution boiled
down to a bootstrap (): Promise<void> method on services that needed to perform an
asynchronous action before it was usable.

As a result, a lot of services ended up like:

@Service([DatabaseService, WebServerService])
export class RootService {
  constructor (protected database: DatabaseService, protected webServer: WebServerService) {}

  async bootstrap () {
    await this.database.bootstrap();
    await this.webServer.bootstrap();
  }
}

There might be a way to solve this in DI.
While it looks like there was a proposal to add asynchronous services in TypeDI, it never emerged.
Additionally, it introduced a substantial amount of complexity into the API.

Another problem with that proposal was forcing all asynchronous services to be "ready"
before any other services could run. This, of course, has the side effect of slowing down the app.
Any important synchronous tasks can't be performed until everything is ready.
This would, of course, quickly bog the application down when a number of async services are introduced.

As a result, I'd like to create a different API that would handle this differently.
Namely, some of the design proposals for this API include:

  • Being able to import asynchronous services without "initialising" them.
  • Not having to wait for every asynchronous service to load before the app can start.
  • Being able to control the order of execution for asynchronous services.

Definitely food for thought.

Interface support

Is your question related to a contrib/ feature?

No

Question

it's will be injection supported using the interface instead of classes in some version, or is it already possible?

Will this work with Typegraphql?

Will this work with Typegraphql and Bun? I will try it tonight and see how far I get. I am also new to DI so not sure about the library or technology as their isn't much documentation on it here.

So yes, big question will this work with Typegraphql?! Thanks for all the awesome work.

Update

It didn't work unfortunately, would love for it to though!

I tried to pass the default {Container} to the graphql builder both Apollo and Graphql Yoga builders and it errors as they expect get() and Typedi does match their interface.

Theres a quick win if you can support Apollo and Graphql yoga you'd have quite a few users of this library as many use Typegraphql in production and are searching for DI libraries that are still being maintained.

Here's a thread on it:

https://www.reddit.com/r/typescript/comments/fo1equ/typedi_cheat_sheet/

Typedi++ would need a small wrapper to match the graphql builder interfaces which would be big win for us as we could easily just switch to using Type++ :))

Update to 0.5.1

I believe version 0.4.0 introduced a regression, which broke code importing
the package -- the TypeScript server was unable to resolve the types for the
imports.

A recent update has been released, 0.5.1, which remedies this problem.
Apologies for the downtime; I'm not entirely familiar with the new exports
system, and I didn't test the package -- I will endeavour to do this in future.

Question: Doubled references when injecting

Is your question related to a contrib/ feature?

No

Question

We're currently using typedi and were looking at esbuild but were stuck due to reflect-metadata not being supported. A friend recommended this library as an alternative and I couldn't help but notice in the example that dependencies need to be referenced twice:

typedi (original - also similar to how it looks in Angular):

@Service()
class RootService {
  constructor(private logger: LogService) {}
}

this library:

@Service([LogService])
class RootService {
  constructor(private logger: LogService) {}
}

The fact that you have to declare all services twice (once in the array of the @Service([]) decorator and again in the constructor) is a bit off putting to me cause it's going to add a lot of boilerplate and it'll require quite a bit of effort to migrate. I know that once I migrate, people are going to forget to maintain both the array + constructor and end up with weird errors and constantly be asking why stuff doesn't work.

Is this a technical limitation of Stage 3 decorators or more of a design decision? One thing that Angular 16 added recently was the ability to inject without the constructor like so:

@Service()
class RootService {
  private logger = inject(LogService);
}

This is nice cause you don't need to match constructor args when extending classes etc.

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.