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

Viewed 6228

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?

5 Answers

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.

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

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

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);

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)
});
Related