Steven Scott
Steven Scott

Reputation: 11250

Typescript working with generics and building an object from a class

I am trying to be able to build a function/method to build an option from an array. For instance, I am using Mikro-ORM, and there is an type call FindOptions<T> which I can populate with my database sort order request.

The FindOptions interface is defined as:

export interface FindOptions<T, P extends string = never> {
    populate?: readonly AutoPath<T, P>[] | boolean;
    orderBy?: (QueryOrderMap<T> & {
        0?: never;
    }) | QueryOrderMap<T>[];
    limit?: number;
    offset?: number;
    ...
}

When working with the FindOptions directly, I can specify my Entity as the generic const MikroORMSoryBy: FindOptions<TimeEntryEntity> which works for code completion and compiler hints, etc.

I can build my sort by doing:

const MikroORMSoryBy: FindOptions<TimeEntryEntity> = {
    orderBy: { EntryDate: QueryOrder.ASC, Id: QueryOrder.ASC, UserId: QueryOrder.ASC }
};

I can clearly list my fields without an issue. I can even list them as strings, and this works.

orderBy: { 'EntryDate': QueryOrder.ASC, 'Id': QueryOrder.ASC, 'UserId': QueryOrder.ASC }

My question is, how can I convert this so that I can build the orderBy using code? For instance, I could pass a string[] of the column names (['EntryDate', 'Id', 'UserId']) and loop through this with a forEach or something.

const Fields: Array<string> = ['EntryDate', 'Id', 'UserId'];
Fields.forEach( (OneField: string) => {
    ...
});

What I do not know how to do though, is build the orderBy, to be built from my string array, instead of having to hard code the names. This would allow me to build a common function for building a sort option.

Ideally, I want something like:

const Fields: Array<string> = ['EntryDate', 'Id', 'UserId'];
Fields.forEach( (OneField: string) => {
    orderBy[OneField] = QueryOption.ASC;  // ???
});

Upvotes: 0

Views: 117

Answers (1)

tenshi
tenshi

Reputation: 26324

We'll start by defining a new function that takes two generic parameters:

function buildOrderBy<E, Fields extends (keyof E)[]>(fields: [...Fields]) => {
    return fields.reduce((r, f) => ({ ...r, [f]: QueryOrder.ASC }), {} as { [K in Fields[number]]: QueryOrder });
}

Simply put, we take the keys of an entity and construct an object of the keys that map to QueryOrder.ASC.

But then if we try to call it here:

const orderByBuilder = buildOrderBy<MyEntity>([...]);

We'll get an error because we did not provide the Fields generic parameter. If we do, then we essentially just duplicated our input to this function, which is terrible. This is called partial type inference (or lack thereof), and one way to get around it is to use currying:

function buildOrderBy<E>() {
    return <Fields extends (keyof E)[]>(fields: [...Fields]) => {
        return fields.reduce((r, f) => ({ ...r, [f]: QueryOrder.ASC }), {} as { [K in Fields[number]]: QueryOrder });
    };
}

Now we can call it like this:

const orderBy = buildOrderBy<MyEntity>()([...]);

Which might be more useful since you can store the intermediary function:

const orderBy = buildOrderBy<MyEntity>();

orderBy([...]); // use independently

Playground

Upvotes: 1

Related Questions