New to electrodb, so apologies if this is obvious or just fundamentally can't be improved.
I'm finding that the types created by electrodb are quite hard to work with in practice in standard Typescript with strict mode enabled and all the recommended eslint rules turned on:
- type signatures are complex and unpredictable
- named types really needed for function signatures, but hard to get
Simplest possible example
I have a class
implementing a service interface, with the DynamoDB client passed into the constructor. I want to create an Entity (or a service) and store it as a member variable for use in methods. What type signature do I use for the member variable?
It's an Entity<something, something, something, something>
.
I've found two acceptable approaches:
1: Observe and accept
Notice that while the type params are not obvious, the schema type is at least recognizable and the whole signature is manageable.
Create a type alias for the schema as just declare the entity using the IDE-provided signature:
private readonly integrations: Entity<string, string, string, SomeSchema>;
2: The factory function gambit
Notice that Typescript will infer function return types and also provides ReturnType<...>
Make a factory function and exploit that feature:
export function SomeEntity(client: electrodb.DocumentClient, table: string) {
return new electrodb.Entity(
{
// ... schema
},
{
client,
table,
},
);
}
private readonly entity: ReturnType<typeof SomeEntity>;
constructor(client: ddb.DynamoDBClient, table: string) {
this.entity = SomeEntity(client, table);
}
Commentary
This is obviously a trivial example but basically illustrates the issue I'm having:
electrodb generates complex type signatures with lots of parameters which presents difficulties when you want to reference them by name, as in this member variable.
Paging Example
So for the next example, let's say we want to expose a simple paging system, which might look like:
export interface ListSomethingOptions {
nextPageToken?: string;
}
export interface SomethingStore {
listSomethingAsync(tenantName: string, options: ListSomethingOptions): Promise<SomethingList>;
// ...
}
The obvious thing for me to do is implement this using the page
method, perhaps by encoding the returned page object into the nextPageToken
(let's say as base64-encoded JSON for this example):
const [next, items] = await this.entity.query
.byIds({ tenantName })
.page(decodePageToken(options.nextPageToken));
I need to implement decodePageToken
, but what type does it return?
It's not obvious. The easiest workaround is to start turning off ESLint:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function decodePageToken(token: string | undefined): any {
return token ? JSON.parse(Buffer.from(token, "base64").toString()) : undefined;
}
But this is probably better though it's equivalent:
function decodePageToken<T>(token: string | undefined): T | undefined {
return token ? (JSON.parse(Buffer.from(token, "base64").toString()) as T) : undefined;
}
Item Mapping Example
Continuing on from the previous example, let's say we now want to map the stored items onto the SomethingList
to be returned. Happily the type signatures mostly match, so writing a reusable map function should be easy... right?
Not so much - because again I've no type name for the fields I'm mapping from.
Doing an inline map is dead easy:
const [next, items] = await this.entity.query
.byIds({ tenantName })
.page(decodePageToken(options.nextPageToken));
return {
nextPageToken: encodePageToken(next),
items: items.map(function (item) {
return {
tenantName: item.tenantName,
id: item.fooId,
// ... about 10 more
};
}),
};
Making it reusable looks much harder though.
Question
I think this is mostly happening because instead of starting with a user-constructed type and then applying magic - as with most popular ORMs, or the standard DocumentClient - we start with a schema and get generated types back.
Do you have some recommendations on the best way to tackle this?
Would it be possible to add more practical Typescript examples into the docs illustrating good usage patterns that minimise this sort of pain - I feel like there's a decent possibility I'm just "holding it wrong" :-)
Many thanks for your efforts on electrodb.