Expose QueryBuilder and Raw classes from the main entry point (deep import into knex/lib/* has no types)
Environment
Knex version: 3.3.0 Database + version: any OS: any TypeScript definitions related — tagging @lorefnon
Feature discussion / request
1. Explain what is your use case
I need a runtime type guard that tells me whether an arbitrary value is a knex query (either a QueryBuilder or a Raw), so I can branch on it before passing it along:
import type { Knex } from 'knex';
export function isKnexQuery(value: unknown): value is Knex.Raw | Knex.QueryBuilder {
return value instanceof KnexBuilder || value instanceof KnexRaw;
}The only way to get the constructors today is a deep import into the package internals:
import KnexBuilder from 'knex/lib/query/querybuilder';
import KnexRaw from 'knex/lib/raw';which TypeScript rejects, because those internal files ship no declarations:
Could not find a declaration file for module 'knex/lib/query/querybuilder'. 'node_modules/knex/lib/query/querybuilder.js' implicitly has an 'any' type.
If the 'knex' package actually exposes this module, try adding a new declaration (.d.ts) file containing `declare module 'knex/lib/query/querybuilder';`The suggested workaround (a hand-written ambient declare module block) is not something I want in my codebase: it types the imports as any, it silently keeps compiling if the internal path is moved or renamed in a future knex release, and ambient module declarations leak into every consumer of my package's own types.
Note that this is specifically about the runtime classes. Knex.QueryBuilder and Knex.Raw already exist in types/index.d.ts, but they are type-only interfaces, so value instanceof Knex.Raw is not expressible.
For Raw there is an undocumented escape hatch — Raw.prototype.isRawInstance = true (lib/raw.js), which knex itself relies on internally (lib/client.js, lib/formatter/wrappingFormatter.js, lib/query/querybuilder.js, …) — but it is not part of the public API, it is not in the type definitions, and there is no equivalent marker for QueryBuilder at all.
There is a related discussion in #6269 about replacing isRawInstance with a Symbol.for()-based check, and a similar request for a different internal class in #4999 (expose FsMigrations). This request is narrower: make the two most commonly needed classes reachable from the main entry point.
2. Explain what kind of feature would support this
Expose the actual QueryBuilder and Raw constructors from the package root, and type them in types/index.d.ts, so that instanceof works without reaching into knex/lib/*.
Today lib/knex-builder/Knex.js already attaches Client as a real class:
knex.Client = Client;but knex.QueryBuilder is deliberately not the class — it is a wrapper object that only carries extend:
knex.QueryBuilder = {
extend: function (methodName, fn) {
QueryBuilder.extend(methodName, fn);
QueryInterface.push(methodName);
},
};and Raw is not exposed at all.
3. Give some API proposal, how the feature should work
Because knex.QueryBuilder is already taken by the extend-only object, replacing it outright would be a breaking change for anyone calling knex.QueryBuilder.extend(...). Two options, in order of preference:
Option A — additive, non-breaking. Export the constructors under distinct names:
// lib/knex-builder/Knex.js
knex.QueryBuilderClass = require('../query/querybuilder');
knex.Raw = require('../raw');// types/index.d.ts
export const QueryBuilderClass: new (...args: any[]) => Knex.QueryBuilder;
export const Raw: new (...args: any[]) => Knex.Raw;Usage:
import { Raw, QueryBuilderClass } from 'knex';
function isKnexQuery(value: unknown): value is Knex.Raw | Knex.QueryBuilder {
return value instanceof QueryBuilderClass || value instanceof Raw;
}(Raw is free today, so it can take the natural name; only the builder needs a non-colliding one. Naming is obviously up for discussion — QueryBuilderClass, QueryBuilderConstructor, or namespacing both under something like knex.classes.* all work for me.)
Option B — ship a first-class type guard instead. If exposing the constructors is not desirable (see #6269 — instanceof is unreliable when two copies of knex end up in the same dependency tree, which is exactly the failure mode the classes would reintroduce for consumers), then a guard exported by knex itself would solve the use case more robustly, since knex can implement it with whatever internal marker it prefers:
import { isKnexQuery, isRaw, isQueryBuilder } from 'knex';
if (isKnexQuery(value)) { /* value: Knex.Raw | Knex.QueryBuilder */ }Option B is arguably the better long-term answer and composes well with the Symbol.for() proposal in #6269; Option A is the smaller change. Either one removes the need for a deep import into knex/lib/*.
Happy to open a PR for whichever direction the maintainers prefer.
Source: knex/knex