I want to register the @aws-sdk/client-ssm with my my tsyring container like all the other aws services I am using. However when I am using "@aws-sdk/client-ssm": "3.435.0" and register it like this:

import { container } from 'tsyringe';
import { SSMClient } from '@aws-sdk/client-ssm';

export const coreContainer = container.createChildContainer();
coreContainer.register(SSMClient, { useValue: new SSMClient({}) });

I get the following error:

TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type  typeof SSMClient  is not assignable to parameter of type  InjectionToken<SSMClient> 
Type  typeof SSMClient  is not assignable to type  constructor<SSMClient> 
Types of construct signatures are incompatible.
Type
new (...[configuration]: [] | [SSMClientConfig]) => SSMClient
is not assignable to type  new (...args: any[]) => SSMClient 
Types of parameters  __0  and  args  are incompatible.
Type  any[]  is not assignable to type  [] | [SSMClientConfig] 
Type  any[]  is not assignable to type  [SSMClientConfig] 
Target requires  1  element(s) but source may have fewer.
dependency-container.d.ts(30, 5): The last overload is declared here.

When I downgrade the @aws-sdk/client-ssm to 3.301.0 it works without any issue.

So I was wondering what the difference is and if it is possible to use the newer version?

I spotted a difference in the constructors of the SSMClient where the old version looks like this: constructor(configuration: SSMClientConfig){

and the newer version looks like this:

constructor(...[configuration]: __CheckOptionalClientConfig<SSMClientConfig>) {

where this __CheckOptionalClientConfig is a type from @smithy/types

But what exactly is the issue here? Can someone explain me this?

1

There are 1 answers

0
Matt Jourard On

Adding to this question since I am currently blocked by this and spent the better part of a day trying to understand the underlying Typescript types and piecing it all together.

tl;dr I couldn't get it to work with regular tokens, so for my clients I went with

container.register<CognitoIdentityProviderClient>(CognitoIdentityProviderClient.toString(), { useFactory: cognitoIdPClientFactory });
container.register<DynamoDBClient>(DynamoDBClient.toString(), { useFactory: dynamoClientFactory });
container.register<S3Client>(S3Client.toString(), { useFactory: s3ClientFactory });
container.register<SSMClient>(SSMClient.toString(), { useFactory: ssmClientFactory });

And it appears to resolve correctly when using the @injectable and @inject annotations on classes that require this client.

The difference between the old version and the updated version of the AWS client library is that wrapping of the __CheckOptionalClientConfig type in the constructor of the aws client class.

For this description, I'll use the CognitoIdentityProviderClient instead of SSMClient since the class is in front of me but they should be functionally identical. For reference, these are the versions I'm on for both libraries from my package-lock.json file:

"node_modules/tsyringe": {
      "version": "4.8.0",
...
"node_modules/@aws-sdk/client-cognito-identity-provider": {
      "version": "3.473.0",

These appear to be the latest versions of both libs at the time of writing. And like the OP, I'm trying to something similar:

import "reflect-metadata";
import { CognitoIdentityProviderClient, CognitoIdentityProviderClientConfig } from "@aws-sdk/client-cognito-identity-provider";
import { container, DependencyContainer } from "tsyringe";

export const cognitoIdPClientFactory = (diContainer: DependencyContainer): CognitoIdentityProviderClient => {
    return new CognitoIdentityProviderClient({});
}


container.register<CognitoIdentityProviderClient>(CognitoIdentityProviderClient, { useFactory: cognitoIdPClientFactory });

Looking at the definition of the the container.register calls, they all look similar to this:

register<T>(token: InjectionToken<T>, provider: FactoryProvider<T>): DependencyContainer;

And the thing that throws the typescript compiler error from the question is the fact that the definition of the aws client constructors don't match up with any of the definitions of InjectionToken.

InjectionToken is defined as this:

declare type InjectionToken<T = any> = constructor<T> | string | symbol | DelayedConstructor<T>;

The only possible values that the client classes can match here would be constructor<T> or DelayedConstructor<T> as the token of CognitoIdentityProviderClient is not a string or symbol.

Looking at constructor<T>, it's defined as:

declare type constructor<T> = {
    new (...args: any[]): T;
};

Fairly straightforward, it should match just about any class constructor since it's essentially saying the constructor can have any number of any type of parameters.

Here is the constructor of CognitoIdentityProviderClient:

constructor(...[configuration]: __CheckOptionalClientConfig<CognitoIdentityProviderClientConfig>);

The left side of the : will mean it's expanding an array of configuration. The array of configuration is what is defined on the right side of the :. So this leads back to what __CheckOptionalClientConfig is.

This is defined as:

/**
 * @public
 *
 * A type which checks if the client configuration is optional.
 * If all entries of the client configuration are optional, it allows client creation without passing any config.
 */
export type CheckOptionalClientConfig<T> = Exact<Partial<T>, T> extends true ? [] | [T] : [T];

Without unwrapping the Exact types, as it isn't super necessary, we see that it resolves to either [] | [T] or [T], so either an empty array or an array containing the single element T.

Applying the type we have to the generic and replacing it back down to simple typescript, we get the constructor to be:

constructor(...[configuration]: [] | [CognitoIdentityProviderClientConfig]);

And so the Typescript compiler complains because you want to fit a function with an either/or of 1 or 0 parameters to an interface with variable ones. Not entirely sure why it doesn't like this, but it doesn't.

I could not get it to work by passing in the regular class name without .toString(). I'm hoping that adding an answer here gives this question some traction, as I couldn't see anything about this on TSyringe github page at all, and to be honest the project seems to be a bit in maintenance mode.