kysely-org / kysely

A type-safe typescript SQL query builder
https://kysely.dev
MIT License
9.82k stars 250 forks source link

Create a generic function with narrowed type in `ExpressionBuilder` #1007

Closed joeledwardson closed 1 month ago

joeledwardson commented 1 month ago

I am trying to use kysely to narrow a type in a function so I can create a generalise function that accepts string column types (see filterString). But typescript is not recognising the value as narrowed to a string type and throwing an error.

I've taken the database tables from the docs for an example.

How can I create a generic function that will allow kysely to recognise a narrowed type of a reference expression so I can call it will a string?

import { ExpressionBuilder } from "kysely";
import { ExtractTypeFromReferenceExpression } from "kysely";
import { SimpleReferenceExpression } from "kysely";
import {
  ColumnType,
  Generated,
  Insertable,
  JSONColumnType,
  Selectable,
  Updateable,
} from "kysely";

export interface Database {
  person: PersonTable;
  pet: PetTable;
}

// This interface describes the `person` table to Kysely. Table
// interfaces should only be used in the `Database` type above
// and never as a result type of a query!. See the `Person`,
// `NewPerson` and `PersonUpdate` types below.
export interface PersonTable {
  // Columns that are generated by the database should be marked
  // using the `Generated` type. This way they are automatically
  // made optional in inserts and updates.
  id: Generated<number>;

  first_name: string;
  gender: "man" | "woman" | "other";

  // If the column is nullable in the database, make its type nullable.
  // Don't use optional properties. Optionality is always determined
  // automatically by Kysely.
  last_name: string | null;

  // You can specify a different type for each operation (select, insert and
  // update) using the `ColumnType<SelectType, InsertType, UpdateType>`
  // wrapper. Here we define a column `created_at` that is selected as
  // a `Date`, can optionally be provided as a `string` in inserts and
  // can never be updated:
  created_at: ColumnType<Date, string | undefined, never>;

  // You can specify JSON columns using the `JSONColumnType` wrapper.
  // It is a shorthand for `ColumnType<T, string, string>`, where T
  // is the type of the JSON object/array retrieved from the database,
  // and the insert and update types are always `string` since you're
  // always stringifying insert/update values.
  metadata: JSONColumnType<{
    login_at: string;
    ip: string | null;
    agent: string | null;
    plan: "free" | "premium";
  }>;
}

// You should not use the table schema interfaces directly. Instead, you should
// use the `Selectable`, `Insertable` and `Updateable` wrappers. These wrappers
// make sure that the correct types are used in each operation.
//
// Most of the time you should trust the type inference and not use explicit
// types at all. These types can be useful when typing function arguments.
export type Person = Selectable<PersonTable>;
export type NewPerson = Insertable<PersonTable>;
export type PersonUpdate = Updateable<PersonTable>;

export interface PetTable {
  id: Generated<number>;
  name: string;
  owner_id: number;
  species: "dog" | "cat";
}

export type Pet = Selectable<PetTable>;
export type NewPet = Insertable<PetTable>;
export type PetUpdate = Updateable<PetTable>;

type PersonEB = ExpressionBuilder<Database, "person">;

// create dummy expression builder
const builder = null as any as PersonEB;

type RE = SimpleReferenceExpression<Database, "person">;

type IsStringColumn<T extends RE> = ExtractTypeFromReferenceExpression<
  Database,
  "person",
  T
> extends string
  ? T
  : never;

type A = IsStringColumn<"first_name">

export function filterString<T extends RE, B extends IsStringColumn<T>>(
  key: B,
  value: string
) {
  /**
   * VALUE is errornous here
   */
  return builder(key, "=", value);
}
koskimas commented 1 month ago

Answered about a billion times.

joeledwardson commented 1 month ago

where?

I did have a look through the docs and issues before writing this and found many asking similar questions regarding type narrowing and filtering but none that match what I'm asking here.

310 and #335 are referring to the returned type of data when a where query is applied, not creating a generic function for columns of a specific type

380 again talks about $narrowType which is referring to the returned type

koskimas commented 1 month ago

Here's just a few I found using a the word generic. There are A LOT more here and in discord. The gist is always the same: extremely strict (Kysely) and generics don't work well together. Strict expects exact types to be known and generic is the opposite.

https://github.com/kysely-org/kysely/issues/997 https://github.com/kysely-org/kysely/issues/995 https://github.com/kysely-org/kysely/issues/670 https://github.com/kysely-org/kysely/issues/642 https://github.com/kysely-org/kysely/issues/496 https://github.com/kysely-org/kysely/issues/385 https://github.com/kysely-org/kysely/issues/122 https://github.com/kysely-org/kysely/issues/109 https://github.com/kysely-org/kysely/issues/43