raveclassic/injectable-ts

Questions: Difficulty with core module's documentation

mrpotatoes opened this issue ยท 5 comments

Question

It looks like this package is maintained I understand it is still in an alpha/beta version but I have a couple of questions that I hope can be answered.

I'm having difficulty understanding a couple of parts of the documentation you put together in the : @injectable-ts/core package.

Logger

The logger doesn't work as expected as documented in the README.md. In order to get it to work I had to call it in this fashion which. It works but I don't understand why this is happening and all attempts to modify the injectable logger function does not work.

The following works

// Any data passed in works to get this to work
logger(1).log('entryPoint(logger)', movies)

Yet this fails with the following error and have no idea how to make this work.

logger.log()

// ERROR
Property 'log' does not exist on type
  'InjectableWithName<{ readonly name: "LOGGER";
    readonly type: Logger;
      readonly optional: true;
        readonly children: []; },
          Logger>'

Injectables Order

I don't quite understand the order of injectable in entryPoint() and why some deps have to be above the other. If I switch them around in main() it throws an error but it doesn't make sense to me. Since this is a functional focused library I figured it was because movieService() required authService() but the code doesn't line up with that fact. I am really confused on this aspect of the library.

Property 'authorize' does not exist on type 'MovieService'.
Property 'fetchMovies' does not exist on type 'AuthService'.

Code Setup

I am on the following Node version: v18.19.1

Package.json

{
  "name": "di-containers",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev:injectable": "ts-node-dev --respawn src/index.ts",
  },
  "dependencies": {
    "@injectable-ts/core": "1.0.0-beta.0",
    "@types/node": "^20.11.28",
    "typescript": "^5.4.2"
  },
  "devDependencies": {
    "ts-node-dev": "^2.0.0"
  }
}

Code

The following code is slightly modifed from the documentation to make it simpler

import { injectable, token } from '@injectable-ts/core'

// --- TOKENS --------------------------------------
interface AuthService {
  authorize(login: string, password: string): string
}

interface MovieService {
  fetchMovies(authToken: string): string[]
}

interface Logger {
  log(...args: readonly unknown[]): void
}

interface EntryPoint {
  (login: string, password: string): Promise<void>
}

// --- INJECTABLES -----------------------------------------
const apiUrl = token('API_URL')<string>()

const logger = injectable('LOGGER', (): Logger => console)

const authService = injectable(apiUrl, (apiUrl: string): AuthService => ({
  authorize: (login: string, password: string): string => `auth token`
}))

const movieService = injectable('MOVIE', apiUrl, (apiUrl: string): MovieService => ({
  fetchMovies: (authToken: string): string[] => ['movie()'],
}))

// --- MAIN ----------------------------------------------
const main = () => {
  console.clear()

  const entryPoint = injectable(
    authService,
    movieService,
    logger,
    (authService, movieService): EntryPoint =>
      async (login, password): Promise<void> => {
        const token = authService.authorize(login, password)
        const movies = movieService.fetchMovies(token)

        logger(1).log('entryPoint(logger)', movies)
        console.log('entryPoint(token):', token)
        console.log('entryPoint(movie):', movies)
      }
  )

  const run = entryPoint({ API_URL: 'https://my-api.com' })
  
  run('John Doe', 'qweqwe')
}

// --- MAIN() OUTPUT -------------------------------
// entryPoint(logger) [ 'movie()' ]
// entryPoint(token): auth token
// entryPoint(movie): [ 'movie()' ]
main()

Regarding Logger

logger in your example is a constructor that returns Logger instance. I can see, relying on your code snippet with type error, that logger does not have any dependencies (readonly children: [];), which means that to get a working instance of Logger you just need to

const loggerInstance = logger({}) // here, `typeof loggerInstance` is `Logger`

The correct way to implement the entryPoint for your example then is to just use logger instance you automatically acquire in the arguments list of projection function of entryPoint (the function that always comes as the last argument in all injectable constructors initialisers), e.g.:

const entryPoint = injectable(
    authService,
    movieService,
    logger,
    (authServiceInstance, movieServiceInstance, loggerInstance): EntryPoint => // <-- notice, here in args list of projection function we get instances of all injectables passed before, and they are all ready to be used
      async (login, password): Promise<void> => {
        const token = authServiceInstance.authorize(login, password)
        const movies = movieServiceInstance.fetchMovies(token)

        loggerInstance.log(movies, token)
      }
  )

Regarding Injectables Order

At this point you probably already get that the order of arguments in projection function is the same as the order of their constructors passed before, but let's wrap it together:

  1. the first argument of entryPoint initialiser is authService injectable, so the first argument of projection function is InjectableValue<typeof authService>;
  2. the second argument of entryPoint initialiser is movieService injectable, so the second argument of projection function is InjectableValue<typeof movieService>;
  3. the third argument of entryPoint initialiser is logger injectable, so the third argument of projection function is InjectableValue<typeof logger>;

Hope this helps :)

BTW, I'm not sure it's documented at the moment, but there's another way to initialise an injectable constructor. You could do it like that:

const entryPoint = injectable(
  {
    authService,
    movieService,
    logger,
  },
  ({ authService, movieService, logger }): EntryPoint =>
    async (login, password): Promise<void> => {
      const token = authService.authorize(login, password)
      const movies = movieService.fetchMovies(token)

      logger.log(movies, token)
    }
)

IMO, that's the more comprehensible way. Here, the order of properties in first argument obviously has no effect on the projection function's argument

Ah, I see. I wouldn't adding the logger to the entryPoint's constructor. Yes, that makes sense. The object version of the injectable is much easier imo. Thank you for the info!

@bukhtiyarov-a-v thanks for taking this over ๐ŸŽ‰

@mrpotatoes thanks for pointing out - I fixed the example ๐Ÿ™