Allow dev to specify the return type that depends upon input parameters at run time in TypeScript

46 Views Asked by At

I'm wondering what options TypeScript has here. I am writing a database query helper, trying to add as much typing as possible. I have a table, MyTable with 3 fields

type MyTable = {
    id: string,
    name: string,
    age: number
}

I want a function that will allow the user to specify which fields to select from the table and then return an array of objects containing just those selected fields.

I got this far:

async QueryDatabaseTable<T = keyof MyTable> (fieldsToReturn: T): Promise<T[]> {
   dbClient.query('myTable').select(Object.keys(fieldsToReturn));
}

So I call QueryDatabaseTable<MyTable>(['name', 'age']) I would expect back an array of objects like:

[{name: 'Romano', age: 33}, {name: 'Geoff', age: 39}]

There is no id, because it wasn't specified.

Basically I want the contract to say whatever the dev passes in to fieldsToReturn, they can expect back an array of objects with fieldsToReturn as the keys.

You obviously wont know until runtime what the dev chooses, but we do know the possible types they can choose from - the keys of MyTable. I'm stuck on how to express that in typescript.

I feel I am mixing run-time and compile-time decisions. I believe if we know the exact fields we want at compile time we can have very strong typing. However, we won't always know exact fields passed in until runtime (user choose fields in a frontend UI when creating a report). However, in the latter case, I believe we can still express that the output of the function should be an array of objects, with keys specified in the fieldsToReturn parameter, even if we do not know the exact fields.

My fallback is to try do something like

export type KeysToArrays<T> = {
    [K in keyof T]: T[K][]
  }

async QueryDatabaseTable (fieldsToReturn: KeysToArrays<MyTableFields>): Promise<Partial<MyTableFields>[]> {
   dbClient.query('myTable').select(fieldsToReturn);
}

but that doesn't tie the output type to the input (string[]).

1

There are 1 best solutions below

0
friartuck On

Alright, after @jcalz's help, I was able to piece together what I really wanted

My ultimate goal was to write a generic handler to querying a single database query for specific values (not range based).

Given I start with my table, MyTable:

type MyTable = {
    id: string,
    name: string,
    age: number
}

I wanted to be able to construct a query object like

queryConfig = {
  tableName: 'my_table',
  filterFields = {'name': ['barbora', 'sandy'] },
  returnFields = ['name', 'age']
}

This should find all rows with the values barbora and sandy

For simplicity you can ignore the next type, it is used to allow filterFields to accept multiple values

type FieldsToArrays<Table> = {
  [Field in keyof Table]: Table[Field][]
}

This is my query queryConfig type:

type QueryTableConfig<Table> = {
    tableName: string,
    filterFields: Partial<FieldsToArrays<Table>>,
    returnFields: (keyof Table)[]
}

And here is putting it all together

async function QueryTable<Table, Return = { [Field in keyof Table]: Table[Field] }
> (config: QueryTableConfig<Table>): Promise<Return[]> {
  console.log(config.filterFields);
  console.log(config.returnFields);
  
  const query = dbQuery<Return>.select(config.returnFields);
  Object.keys(config.filterFields).forEach(f => {
    query.whereIn(f, config.filterFields[f])
  });
  return query.execute();
}

And then executing it. We specify the table's type (MyTable) as a type parameter when invoking the function

QueryTable<MyTable>({
  tableName: 'test',
  returnFields: ['id', 'name'],
  filterFields: {
    name:  ['Ramano', 'Geoff'],
    id: ['some-random-uuid']
    }
});

As said it won't do range based comparisons, but for many of my queries that is fine

Thanks again for @jcalz's initial solution - set me on the right path