Andy
Andy

Reputation: 4778

How can I map a DynamoDB AttributeMap type to an interface?

Say I have a typescript interface:

interface IPerson {
    id: string,
    name: string
}

And I run a table scan on a persons table in dynamo, what I want to be able to do is this:

const client = new AWS.DynamoDB.DocumentClient();

client.scan(params, (error, result) => {
    const people: IPerson[] = result.Items as IPerson[];
};

I am getting the error Type 'AttributeMap[]' cannot be converted to type 'IPerson[]'

Obviously they are different types, however the data structure is exactly the same. My question is how can I essentially cast the dynamo AttributeMap to my IPerson interface?

Upvotes: 15

Views: 11211

Answers (7)

Vinicius
Vinicius

Reputation: 591

You could use class-transformer. If you need some property not be shown you can annotate with @Exclude() and then use plainToClass function. It can be easily installed in your local system or node-project using the Node Package Manager:

npm install class-transformer
interface IPerson {
    id: string,
    name: string
    @Exclude()
    password: string
}

// for an object
const person = plainToClass(IPerson, response.Item)

// for an array
const people = response.Items.map((item) => plainToClass(IPerson, item));

Or even simpler, use Object.assign:

interface IPerson {
    id: string,
    name: string
}

// for an object
const person = Object.assign(IPerson, response.Item);

//for ann array
const people = response.Items.map((item) => Object.assign(IPerson), item);

Upvotes: 0

Stretch0
Stretch0

Reputation: 9283

Unfortunately the aws-sdk library does not provider a Generic interface for you to set the response type. So you can't do something like:

const person = await client.get<IPerson>(params).promise();

As others have mentioned, you can cast the type using something like:

const person = result?.Item as IPerson;

The risk with this is result?.Item may not be the type of IPerson and you are tricking Typescript into thinking it is.

Because of the schemaless nature of DynamoDB, the item in your DB could technically be of any shape so it might not include the id and name attributes you require. Therefore, it would be prudent to have some checks in place incase the data isn't as you expect.

One way to do that is to use a formatter that maps your dynamoDB object to your expected response:

const client = new AWS.DynamoDB.DocumentClient();

interface IPerson {
  id?: string,
  name?: string
}

const format = (data: DynamoDB.DocumentClient.AttributeMap): IPerson {
  return {
    id: data?.id,
    name: data?.name
  };
} 

const person = await client.get(params).promise();
      
const response = format(basket.Item); // maps DynamoDB.DocumentClient.AttributeMap => IPerson

DynamoDB.DocumentClient.AttributeMap is the response type of client.get but scan or other client methods might have a different response type. However, the same principle still stands of mapping the response of the client to the type you require.

Upvotes: 1

Colm Bhandal
Colm Bhandal

Reputation: 3851

I had a similar problem but was using get rather than scan. I solved this problem using a type guard a.k.a. type predicate. I am using v2 of the AWS SDK and I am using an object of type AWS.DynamoDB.DocumentClient to read from the DynamoDB table. I first tried to use unmarshall as suggested in Steven's answer, but this resulted in an empty object. Seems like the client I was using was already unmarshalling the object for me.

In this case, the problem reduces to a standard one: you have an object of type any and you want to convert it some some type IPerson. A standard way to do this is to use a type predicate. Something like this:

function isPerson(obj: any): obj is IPerson{
    if(!obj){
        return false
    }
    if(!isSimpleProperty(obj, "id") || !isString(obj.id)){
        return false
    }
    if(!isSimpleProperty(obj, "name") || !isString(obj.name)){
        return false
    }
    return true
}

function isString(obj: any){
    return (typeof obj === 'string' || obj instanceof String)
}

function isSimpleProperty(obj: any, property: PropertyKey){
    const desc = Object.getOwnPropertyDescriptor(obj, property)
    if(!!desc){
        return (!desc.get && !desc.set)
    }
    return false
}

Then you can use a filter method to get back all the items you want, of the correct type:

const client = new AWS.DynamoDB.DocumentClient();

client.scan(params, (error, result) => {
    const people: IPerson[] | undefined = result.Items?.filter(isPerson)
});

Upvotes: 1

Willian
Willian

Reputation: 3405

It seems there is no built-in serialization for aws-sdk.

What I've done so far is like below, it works fine:

interface IPerson {
    id: string,
    name: string
}

// for single object
const person = result?.Item as IPerson;

// for arrays
const people = result?.Items?.map((item) => item as IPerson);

Upvotes: 1

Steven  Shang
Steven Shang

Reputation: 107

The right way to do this is by using the AWS DynamoDB SDK unmarshall method.


JavaScript AWS SDK V3 (post December 2020)

Use the unmarshall method from the @aws-sdk/util-dynamodb package.

Docs Example.

const { unmarshall } = require("@aws-sdk/util-dynamodb");

unmarshall(res.Item) as Type;

Side note: the AWS DynamoDB JavaScript SDK provides a DynamoDBDocumentClient which removes this whole problem and uses normal key value objects instead.


The previous version of the JavaScript AWS SDK (pre December 2020)

Use the AWS.DynamoDB.Converter:

// Cast the result items to a type.
const people: IPerson[] = result.Items?.map((item) => Converter.unmarshall(item) as IPerson);

Doc for unmarshall():

unmarshall(data, options) ⇒ map

Convert a DynamoDB record into a JavaScript object.

Side note: the AWS DynamoDB JavaScript SDK provides a DynamoDBDocumentClient which removes this whole problem and uses normal key value objects instead.

Upvotes: 5

guijob
guijob

Reputation: 4488

I've just solve it by casting to unknown first:

const people: IPerson[] = result.Items as unknown as IPerson[];

Upvotes: 3

BenjaminPaul
BenjaminPaul

Reputation: 2941

Extend the IPerson interface with AttributeMap like so:

interface IPerson extends AttributeMap {
    id: string,
    name: string
}

Upvotes: 1

Related Questions