typesafe-dynamodb
typesafe-dynamodb copied to clipboard
Incorrect type inference from GetItem command (v3)
The type of Item appears to be undefined. I expected it to be IAccount

Here is the code
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const client1 = new DynamoDBClient({});
export const docClient = DynamoDBDocumentClient.from(
client1
) as TypeSafeDocumentClientV3<IAccount, 'pk', 'sk'>;
const res1 = await docClient.send(
new TypedGetItemCommand({
TableName: 'Account',
Key: {
pk: 'ACCOUNT#foo',
sk: 'ACCOUNT#foo',
},
})
);
res1.Item;
How is TypedGetItemCommand
defined?
export const TypedGetItemCommand = TypeSafeGetDocumentCommand<
IAccount,
'pk',
'sk'
>();
Hmm, I can't seem to reproduce this error.
Tried the same code in a separate package to make sure it works when importing typesafe-dynamodb
from node_modules
and it works.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { TypeSafeDocumentClientV3 } from "typesafe-dynamodb/lib/document-client-v3";
import { TypeSafeGetDocumentCommand } from "typesafe-dynamodb/lib/get-document-command";
const client1 = new DynamoDBClient({});
export interface IAccount {
pk: string;
sk: string;
}
export const docClient = DynamoDBDocumentClient.from(
client1
) as TypeSafeDocumentClientV3<IAccount, "pk", "sk">;
export const TypedGetItemCommand = TypeSafeGetDocumentCommand<
IAccount,
"pk",
"sk"
>();
async function main() {
const res1 = await docClient.send(
new TypedGetItemCommand({
TableName: "Account",
Key: {
pk: "ACCOUNT#foo",
sk: "ACCOUNT#foo",
},
})
);
res1.Item;
}
The problem is in the generics. Try with this type:
export interface IAccount<
id extends string = string,
key extends string = string
> {
pk: `ACCOUNT${id}`;
sk: `ACCOUNT${key}`;
}
Got it, now I can repro. Will dig in.
Hmm, it works when the keys are parameterized but not when hard-coded.
export interface IAccount<
id extends string = string,
key extends string = string
> {
pk: `ACCOUNT#${id}`;
sk: `ACCOUNT#${key}`;
}
const GetUnionItemCommand = TypeSafeGetDocumentCommand<IAccount, "pk", "sk">();
export async function union(pk: string, sk: string) {
const item = await client.send(
new GetUnionItemCommand({
TableName: "foo",
Key: {
pk: `ACCOUNT#${pk}`,
sk: `ACCOUNT#${sk}`,
},
})
);
item.Item?.pk;
}
Narrowed down the problem to Narrow
.
// is `never` because TS isn't smart enough to detect the overlap
type A = Extract<
IAccount,
{
pk: "ACCOUNT#pk";
sk: `ACCOUNT#${string}`;
}
>
This may be a limitation of string literal types in typescript.

The reason for why Item
is undefined
in your example is because never | undefined
reduces to undefined
.
The workaround is to not use hard-coded strings.
const pk: string = "pk";
const sk: string = "sk";
const item = await client.send(
new GetUnionItemCommand({
TableName: "foo",
Key: {
pk: `ACCOUNT#${pk}`,
sk: `ACCOUNT#${sk}`,
},
})
);
Note how I store the strings as const pk: string
and especially note how i explicitly type it as : string
. This is so that TypeScript will properly infer the type as ACCOUNT#${string}
.
I'll keep digging to see if there is something we can to workaround this in typesafe-dynamdob.
I don't think we can workaround this issue. It is not an expected use-case to have hard-coded suffixes in the keys that aren't properly represented in the type (IAccount
). It will only work if the types are parameterized.
You can see the general problem here - ACCOUNT#${string}
is not assignable to ACCOUNT#abc
and therefore narrows to never
. Maybe TS could be smarter here and realize that ACCOUNT#${string}
should be narrowed to the literal string ACCOUNT#abc
. I will cut an issue with the TypeScript team and see what they think.

Yeah, that is a bummer.
We will see what the TypeScript team has to say about this https://github.com/microsoft/TypeScript/issues/49617
Need to try intersection (&) instead of Extract
Intersection types do seem to behave better here.
This issue is now marked as stale because it hasn't seen activity for a while. Add a comment or it will be closed soon. If you wish to exclude this issue from being marked as stale, add the "backlog" label.
Closing this issue as it hasn't seen activity for a while. Please add a comment @mentioning a maintainer to reopen. If you wish to exclude this issue from being marked as stale, add the "backlog" label.