Reputation: 4778
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
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
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
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
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
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.
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
Reputation: 4488
I've just solve it by casting to unknown
first:
const people: IPerson[] = result.Items as unknown as IPerson[];
Upvotes: 3
Reputation: 2941
Extend the IPerson interface with AttributeMap like so:
interface IPerson extends AttributeMap {
id: string,
name: string
}
Upvotes: 1