Search code examples
typescriptamazon-web-servicesaws-sdktypescript-types

AWS DynamoDB V3 SDK How to write generic send?


I am using Typescript with AWS SDK V3. I would like to create DynamoDB client send wrapper that would do error logging. The problem is that I can't pass DynamoDB commands due to a type mismatch. Example:


import {
  DynamoDBClient,
  UpdateItemCommand,
} from '@aws-sdk/client-dynamodb';


type CommandType = Parameters<InstanceType<typeof DynamoDBClient>['send']>[0];

function fn(cmd: CommandType) {
  const client = new DynamoDBClient({ region: 'us-west-2' });
  client.send(cmd);
}

const updateItemCommand = new UpdateItemCommand({ ... });

fn(updateItemCommand)

I get and error at fn(updateItemCommand):

Type 'UpdateItemCommand' is not assignable to type 'Command<ServiceInputTypes, ServiceInputTypes, ServiceOutputTypes, ServiceOutputTypes, SmithyResolvedConfiguration<HttpHandlerOptions>>'.
  The types of 'middlewareStack.add' are incompatible between these types.
    Type '{ (middleware: InitializeMiddleware<UpdateItemCommandInput, UpdateItemCommandOutput>, options?: (InitializeHandlerOptions & AbsoluteLocation) | undefined): void; (middleware: SerializeMiddleware<...>, options: SerializeHandlerOptions & AbsoluteLocation): void; (middleware: BuildMiddleware<...>, options: BuildHandler...' is not assignable to type '{ (middleware: InitializeMiddleware<ServiceInputTypes, ServiceOutputTypes>, options?: (InitializeHandlerOptions & AbsoluteLocation) | undefined): void; (middleware: SerializeMiddleware<...>, options: SerializeHandlerOptions & AbsoluteLocation): void; (middleware: BuildMiddleware<...>, options: BuildHandlerOptions & ...'.
      Types of parameters 'middleware' and 'middleware' are incompatible.
        Types of parameters 'next' and 'next' are incompatible.
          Type 'InitializeHandler<UpdateItemCommandInput, UpdateItemCommandOutput>' is not assignable to type 'InitializeHandler<ServiceInputTypes, ServiceOutputTypes>'.
            Type 'ServiceInputTypes' is not assignable to type 'UpdateItemCommandInput'.ts(2322)
lala.ts(122, 38): The expected type comes from property 'command' which is declared here on type '{ command: Command<ServiceInputTypes, ServiceInputTypes, ServiceOutputTypes, ServiceOutputTypes, SmithyResolvedConfiguration<...>>; }'

Which is strange since this example works fine:

import {
  DynamoDBClient,
  UpdateItemCommand,
} from '@aws-sdk/client-dynamodb';

const updateItemCommand = new UpdateItemCommand({ ... });
const client = new DynamoDBClient({ region: 'us-west-2' });
client.send(updateItemCommand);

Solution

  • Crucial things to note with how this send is defined:

    import {
      Client as __Client,
      // …
    } from "@aws-sdk/smithy-client";
    
    // …
    
    export class DynamoDBClient extends __Client<
      __HttpHandlerOptions,
      ServiceInputTypes,
      ServiceOutputTypes,
      DynamoDBClientResolvedConfig
    > {
      // …
    }
    
    export class Client<
      HandlerOptions,
      ClientInput extends object,
      ClientOutput extends MetadataBearer,
      ResolvedClientConfiguration extends SmithyResolvedConfiguration<HandlerOptions>
    > implements IClient<ClientInput, ClientOutput, ResolvedClientConfiguration>
    {
      // …
      send<InputType extends ClientInput, OutputType extends ClientOutput>(
        command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
        options?: HandlerOptions
      ): Promise<OutputType>;
      send<InputType extends ClientInput, OutputType extends ClientOutput>(
        command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
        cb: (err: any, data?: OutputType) => void
      ): void;
      send<InputType extends ClientInput, OutputType extends ClientOutput>(
        command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
        options: HandlerOptions,
        cb: (err: any, data?: OutputType) => void
      ): void;
      send<InputType extends ClientInput, OutputType extends ClientOutput>(
        command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
        optionsOrCb?: HandlerOptions | ((err: any, data?: OutputType) => void),
        cb?: (err: any, data?: OutputType) => void
      ): Promise<OutputType> | void {
        // …
      }
    
      // …
    }
    

    Note that send is generic and overloaded. That means your type alias is not capturing the full story. And unfortunately, you can’t capture the full story in a type. I’m pretty sure it would require higher-order types to do it, and that request has sat in the TS bug tracker for a long, long time (it really complicates a language so the TS team seems to want to try to see just how far they can get without it).

    Which means you’re left just copying and pasting the signature for send, albeit with the appropriate types from DynamoDBClient:

    import {
      DynamoDBClient,
      ServiceInputTypes,
      ServiceOutputTypes,
    } from '@aws-sdk/client-dynamodb';
    import { SmithyResolvedConfiguration } from '@aws-sdk/smithy-client';
    import { Command, HttpHandlerOptions } from '@aws-sdk/types';
    
    export function fn<InputType extends ServiceInputTypes, OutputType extends ServiceOutputTypes>(
      command: Command<ServiceInputTypes, InputType, ServiceOutputTypes, OutputType, SmithyResolvedConfiguration<HttpHandlerOptions>>,
      options?: HttpHandlerOptions
    ): Promise<OutputType>;
    export function fn<InputType extends ServiceInputTypes, OutputType extends ServiceOutputTypes>(
      command: Command<ServiceInputTypes, InputType, ServiceOutputTypes, OutputType, SmithyResolvedConfiguration<HttpHandlerOptions>>,
      cb: (err: any, data?: OutputType) => void
    ): void;
    export function fn<InputType extends ServiceInputTypes, OutputType extends ServiceOutputTypes>(
      command: Command<ServiceInputTypes, InputType, ServiceOutputTypes, OutputType, SmithyResolvedConfiguration<HttpHandlerOptions>>,
      options: HttpHandlerOptions,
      cb: (err: any, data?: OutputType) => void
    ): void;
    export function fn<InputType extends ServiceInputTypes, OutputType extends ServiceOutputTypes>(
      command: Command<ServiceInputTypes, InputType, ServiceOutputTypes, OutputType, SmithyResolvedConfiguration<HttpHandlerOptions>>,
      optionsOrCb?: HttpHandlerOptions | (err: any, data?: OutputType) => void,
      cb?: (err: any, data?: OutputType) => void
    ): void {
      const client = new DynamoDBClient({ region: 'us-west-2' });
      client.send(cmd, optionsOrCb, cb);
    }
    

    You don’t need all of the overloads, though. So for example, this works, assuming you don’t want to use the callback version:

    import {
      DynamoDBClient,
      ServiceInputTypes,
      ServiceOutputTypes,
    } from '@aws-sdk/client-dynamodb';
    import { SmithyResolvedConfiguration } from '@aws-sdk/smithy-client';
    import { Command, HttpHandlerOptions } from '@aws-sdk/types';
    
    export function fn<InputType extends ServiceInputTypes, OutputType extends ServiceOutputTypes>(
      command: Command<
        ServiceInputTypes,
        InputType,
        ServiceOutputTypes,
        OutputType,
        SmithyResolvedConfiguration<HttpHandlerOptions>
      >,
      options?: HttpHandlerOptions,
    ): Promise<OutputType> {
      const client = new DynamoDBClient({ region: 'us-west-2' });
      return client.send(command, options);
    }
    

    Then this works:

    import {
      BatchWriteItemCommand,
      BatchWriteItemCommandInput,
      GetItemCommand,
      GetItemCommandInput,
      UpdateItemCommand,
      UpdateItemInput,
    } from '@aws-sdk/client-dynamodb';
    import { fn } from 'wherever';
    
    const updateItemCmd = new UpdateItemCommand({} as UpdateItemInput);
    const batchWriteItemCmd = new BatchWriteItemCommand({} as BatchWriteItemCommandInput);
    const getItemCmd = new GetItemCommand({} as GetItemCommandInput);
    
    fn(updateItemCmd);
    fn(batchWriteItemCmd);
    fn(getItemCmd);