Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: [typed-inject in Clean Architecture] How to inject instances so that other registered instances can reference them while being used as injectables themselves by other instances? #59

Open
haukehem opened this issue Apr 23, 2023 · 1 comment

Comments

@haukehem
Copy link

Hi! First of all, I really appreciate this project and it's type-safe DI approach to Typescript. However, while implementing a little demonstration project with a Clean Architecture approach, I could not figure out how to correctly set up my IoC container.

My question sounds a bit long-winded, but let my try to describe my approach.

Short description of my object relation / call flow:
UI/reducer -> Use-Case -> Service (impl. by a Repository) -> Datasource -> API client
To minimize the amount dependencies, increase performance and overall be more flexible/adaptable, in my UI, I want to get the injected Use-Case instance, which in itself references a Service registered in the IoC and so on...

Now, to my approach. I started by implementing my API client:

export interface ApiClient {
    get(url: string): Promise<any>;

    post(url: string, body: {},
         contentType: RequestContentType,
         authenticated: boolean): Promise<any>;
}

export class BlueprintOffersApiClient implements ApiClient {
    async get(url: string) {
       ...
    }

    async post(url: string, body: {},
               contentType: RequestContentType = RequestContentType.json,
               authenticated: boolean = true) {
        ...
    }

    private static async getAccessToken(): Promise<string> {
        ...
    }
}

Next, my API Datasource of my Account module:

export interface IAccountApiDatasource {
    getUserProfile(id: string): Promise<UserProfile>;

    directToSignIn(codeChallenge: string): void;

    authenticate(codeVerifier: string, authorizationCode: string, scope: string): Promise<Credentials>;

    directToSignOut(idToken: string): void;
}

export class AccountApiDatasource implements IAccountApiDatasource {
    public static inject = [Types.apiClient] as const;

    constructor(private readonly apiClient: ApiClient
    ) {
    }

    async getUserProfile(id: string): Promise<UserProfile> {
       ...
    }

    async authenticate(codeVerifier: string, authorizationCode: string, scope: string): Promise<Credentials> {
        ...
    }

    directToSignOut(idToken: string): void {
        ...
    }

    directToSignIn(codeChallenge: string): void {
        ...
    }
}

In my Account module, I have two other Datasource NOT referencing the API client or any other thing:

export interface IAccountLocalDatasource {
    ...
}

export class AccountLocalDatasource implements IAccountLocalDatasource {
    ...
}
export interface IAccountSessionDatasource {
    ...
}

export class AccountSessionDatasource implements IAccountSessionDatasource {
    ...
}

The Datasources are used in the module's Repositories (Service implementations):

export interface AuthenticationService {
    signIn(): void;

    authenticate(): Promise<void>;

    signOut(): void;
}
export class AuthenticationRepository implements AuthenticationService {
    public static inject = [Types.iAccountApiDatasource, Types.iAccountLocalDatasource, Types.iAccountSessionDatasource] as const;

    constructor(private readonly iAccountApiDatasource: IAccountApiDatasource,
                private readonly iAccountLocalDatasource: IAccountLocalDatasource,
                private readonly iAccountSessionDatasource: IAccountSessionDatasource) {
    }

    async authenticate(): Promise<void> {
       ...
    }

    signOut(): void {
        ...
    }

    signIn(): void {
        ...
    }
}

The Use-Cases then reference the Services:

export class SignIn {
    public static inject = [Types.authenticationService] as const;

    constructor(private readonly authenticationService: AuthenticationService) {
    }

    async invoke() : Promise<void> {
        return this.authenticationService.signIn();
    }
}

Ok, so far so good. Now I want to set up my IoC:

export class Types {
    // Clients
    static apiClient = "apiClient";

    // Data sources
    static iAccountApiDatasource = "iAccountApiDatasource";
    static iAccountLocalDatasource = "iAccountLocalDatasource";
    static iAccountSessionDatasource = "iAccountSessionDatasource";
    ...

    // Services
    static authenticationService = "authenticationService";
   ...

    // Use cases
    static signIn = "signIn";
    ...
}

// Called in index.tsx
export function initializeDependencies() {
    return createInjector().provideClass(Types.apiClient, BlueprintOffersApiClient, Scope.Singleton)
        .provideClass(Types.iAccountApiDatasource, AccountApiDatasource, Scope.Singleton)
        .provideClass(Types.iAccountLocalDatasource, AccountLocalDatasource, Scope.Singleton)
        .provideClass(Types.iAccountSessionDatasource, AccountSessionDatasource, Scope.Singleton)
        .provideClass(Types.authenticationService, AuthenticationRepository, Scope.Singleton)
        .provideClass(Types.signIn, SignIn, Scope.Singleton);
}

But with this setup, the compiler displays the following error in the line providing the AuthenticationRepository:
TS2345: Argument of type 'typeof AuthenticationRepository' is not assignable to parameter of type 'InjectableClass {}, BlueprintOffersApiClient, string>, AccountApiDatasource, string>, AccountLocalDatasource, string>, AccountSessionDatasource, string>, AuthenticationRepository, readonly [...]>'.

So, to get back to my (multipart) question:

  1. What is the error trying to tell me and and why does it occur - I really want to understand it
  2. How can I inject my API client(s), Datasources, Repositories and Use-Cases instances so that other registered instances can reference them while being used as injectables themselves by "higher" (closer to UI) instances? - But I also need a solution:D
@haukehem
Copy link
Author

After trying out some things, I could get rid of the compiler errors in my IoC by instanciating all dependencies and wiring the stuff manually:

export function initializeDependencies() {
    const container = createInjector().provideClass(Types.apiClient, BlueprintOffersApiClient);

    // Data sources
    const accountApiDatasource = new AccountApiDatasource(container.resolve(Types.apiClient))
    container.provideValue(Types.iAccountApiDatasource, accountApiDatasource);
    const accountLocalDatasource = new AccountLocalDatasource();
    container.provideValue(Types.iAccountLocalDatasource, accountLocalDatasource);
    const accountSessionDatasource = new AccountSessionDatasource();
    container.provideValue(Types.iAccountSessionDatasource, accountSessionDatasource);

    // Repositories
    const authenticationRepository = new AuthenticationRepository(
        accountApiDatasource,
        accountLocalDatasource,
        accountSessionDatasource
    )
    container.provideValue(Types.authenticationService, authenticationRepository);

    // Use-Cases
    const signIn = new SignIn(authenticationRepository);
    container.provideValue(Types.signIn, signIn);
    return container;
}

But not only is this a pretty strange solutin on its own. Now, when I try to resolve a dependency (here: Calling SignIn-Use-Case in UI), the type of the instance I want to resolve is interpretated as BlueprintOffersApiClient, my API client implementation:

export default function SignInViewModel() {
    const signIn = container.resolve(Types.signIn);

    async function onSignIn() {
        await signIn.invoke();
    }
    ...
}

The compiler throws the following error for await signIn.invoke();:
TS2339: Property 'invoke' does not exist on type 'BlueprintOffersApiClient'.

Probably the way I tried it here is not the idea of the plugin. Just want to protocoll what I've tried and what errors are displayed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant