How to properly build an array of keys which match a type's keys in TypeScript?

1.6k Views Asked by At

I am using the Kysely SQL builder for JS (as per Vercel's recommendation, although the docs/community is sparse). It is a fully typed SQL builder. You construct a db object with a schema, and when you do queries it recognizes the table names and attributes (pretty fancy if I do say so myself).

import 'dotenv/config'
import { createKysely } from '@vercel/postgres-kysely'
import { DB } from 'kysely-codegen'

export const db = createKysely<DB>()
export { sql } from 'kysely'

The DB is generated from the PostgreSQL schema directly, and stored in the kysely-codegen node_modules folder. It looks like this (short snippet):

export interface MyTable {
  id: string
  foo: string
  bar: boolean
}

export interface DB {
  my_table: MyTable
}

Now, my question revolves around using the select function, which takes an array of keys of MyTable (when querying my_table).

const record = await db
  .selectFrom('my_table')
  .select(['foo', 'id'])
  .executeTakeFirst()

That works fine. But it doesn't work when I do this:

// simulate not knowing what the input is
// as if you were making a browser JSON API request
const json = JSON.parse(fs.readFileSync('test.json'))
const selectKeys = Object.keys(json)

const record = await db
  .selectFrom('my_table')
  .select(selectKeys)
  .executeTakeFirst()

I get essentially this error:

Argument of type 'string[]' is not assignable to parameter of type 'SelectArg<DB, "my_table", SelectExpression<DB, "my_table">>'.

I can fix it like this:

const record = await db
  .selectFrom('my_table')
  .select(selectKeys as Array<keyof MyTable>)
  .executeTakeFirst()

I can also guarantee somewhere earlier in the code that the selectKeys is built with the keys of my_table, by doing something like this:

const MY_TABLE_KEYS: Array<keyof MyTable> = ['id', 'foo', 'bar']

function getKeys(json) {
  const keys = []
  for (const key in json) {
    if (MY_TABLE_KEYS.includes(key)) {
      keys.push(key)
    }
  }
  return keys
}

I have basically duplicated/copied the keys from the DB interface into an array, but there are several problems with the getKeys function, not sure a way around it. A rough TS playground is here demonstrating some of the challenges I'm facing.

The question is, how can I properly type the keys to pass to the .select method?

1

There are 1 best solutions below

0
On

You might be looking for a type predicate / type guard. Type predicates are functions that assert/declare that an argument is a specific type if the function returns true. In this case a simple predicate might look like

(key: string): key is keyof MyTable => { return true; }

In the simple example above, I always return true. To make this useful you would wan't to check if the key is actually one of the keys you know about. You can use this example below to get what you're looking for.

interface MyTable {
    id: string;
    foo: string;
    bar: boolean;
}

const json: Partial<MyTable> = { id: "hello", foo: "world" };

// Get keys from json, then filter out all non-MyTable-keys
// our filter is a predicate
const keys = Object.keys(json).filter((key): key is keyof MyTable =>
    ["id", "foo", "bar"].includes(key),
);

const record: Partial<MyTable> = {};
keys.forEach((key) => (record[key] = json[key]));

You'll notice that you still have some issues with the record[key] = json[key] but that's because json[key] may be undefined (because it's a partial);