There is currently a "recipe" of how to write a plugin (essentially a mixin) using regular JavaScript but I think it would be nice to have a TypeScript example just like you do for the query builder.
It's basically impossible to type plugins unfortunately. Especially if multiple plugins are used. There's no way to extend an existing dynamic type in typescript. A plugin is a function that takes a model class constructor and returns a new constructor inherited from the input class:
function somePlugin<T extends Model>(modelClass: ModelClass<T>): ??? {
return class extends modelClass {
...
}
}
The ??? part is impossible to type so that the plugin works for both
class Person extends somePlugin(Model) {
}
and
class Person extends somePlugin(someOtherPlugin(Model)) {
}
If the plugin doesn't add any new methods to neither the QueryBuilder nor the Model, the typing is trivial: just return the input type:
function somePlugin<T extends typeof Model>(modelClass: T): T {
...
}
That is, unfortunately, what I have come to realize after many hours of trying to type them. Even after lowering my expectations and peppering a few @ts-ignores around, it doesn't seem doable in a way that isn't totally gross. I was hoping with the addition of mixin support in TS it would be doable, but it does seem like anything that is non-trivial is just not doable cleanly.
As an aside, one thing I was using with some level of success before was ModelClass, which now seems to be an empty interface.
Is it no longer required to have that duplication? I believe it was there before to deal with static class members.
The old ModelClass is now replaced with typeof Model in most places.
That is what I thought. Thanks.
I'd like to write some docs anyway with some examples along with the drawbacks. Would that be okay?
hmm... I was able to create this.. Maybe it's possible nowadays after all
import { Model, QueryBuilder, Page } from './typings/objection';
type Constructor<T extends Model> = new (...args: any[]) => T;
class CustomQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
ArrayQueryBuilderType!: CustomQueryBuilder<M, M[]>;
SingleQueryBuilderType!: CustomQueryBuilder<M, M>;
NumberQueryBuilderType!: CustomQueryBuilder<M, number>;
PageQueryBuilderType!: CustomQueryBuilder<M, Page<M>>;
someCustomMethod(): this {
return this;
}
}
function mixin<T extends Constructor<Model>>(ModelClass: T) {
return class extends ModelClass {
static QueryBuilder = QueryBuilder;
QueryBuilderType: CustomQueryBuilder<this, this[]>;
mixinMethod() {}
};
}
class Person extends Model {}
const MixinPerson = mixin(Person);
async () => {
const z = await MixinPerson.query()
.whereIn('id', [1, 2])
.someCustomMethod()
.where('foo', 1)
.someCustomMethod();
z[0].mixinMethod();
};
This was with typescript 3.7.2
That is actually _really close_ to what I have. I'll update my stuff to reflect your example and let you know if there are any issues. I am also on 3.7.2.
So here is the example I am working with. I'll annotate with the errors I am seeing:
import { Model } from 'objection';
export type ModelConstructor<T extends Model = Model> = new (
...args: any[]
) => T;
export function Mixin(options = {}) {
return function<T extends ModelConstructor>(Base: T) {
class CustomQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
ArrayQueryBuilderType!: CustomQueryBuilder<M, M[]>;
SingleQueryBuilderType!: CustomQueryBuilder<M, M>;
NumberQueryBuilderType!: CustomQueryBuilder<M, number>;
PageQueryBuilderType!: CustomQueryBuilder<M, Page<M>>;
// Class 'QueryBuilder<M, R>' defines instance member property 'delete', but extended class 'CustomQueryBuilder<M, R>' defines it as instance member function.ts(2425)
delete() {
const value = new Date();
// Argument of type '{ [x: string]: unknown; }' is not assignable to parameter of type 'PartialModelObject<this["ModelType"]>'.ts(2345)
return this.patch({
"deleted_at": value
});
}
kept(): this {
// Property 'ref' does not exist on type 'T'.ts(2339)
return this.whereNull(Base.ref(column));
}
discarded(): this {
// Property 'ref' does not exist on type 'T'.ts(2339)
return this.whereNotNull(Base.ref(column));
}
}
return class extends Base {
static QueryBuilder = CustomQueryBuilder;
QueryBuilderType!: CustomQueryBuilder<this, this[]>;
static get modifiers(): Modifiers<CustomQueryBuilder<Model>> {
return {
// Property 'modifiers' does not exist on type 'T'.ts(2339)
...super.modifiers,
discarded(builder) {
builder.discarded();
},
kept(builder) {
builder.kept();
}
};
}
};
};
}
Based on these errors, it feels like TypeScript just can't figure out the correct type for T here. It can't figure out any static or instance properties. When I type Base. this is the autocomplete I get - basically just function methods and properties. It has no additional Objection properties or methods.

The ModelConstructor doesn't have any properties or methods so that's completely understandable. How about
export function Mixin(options = {}) {
return function<T extends typeof Model>(Base: T) {
I was using T extends typeof Model before your example, and that got rid of most of the errors there. The remaining errors were these two:
// Class 'QueryBuilder<M, R>' defines instance member property 'delete', but extended class 'CustomQueryBuilder<M, R>' defines it as instance member function.ts(2425)
delete() {
const value = new Date();
// Argument of type '{ [x: string]: unknown; }' is not assignable to parameter of type 'PartialModelObject<this["ModelType"]>'.ts(2345)
return this.patch({
"deleted_at": value
});
}
The second error ts(2345) makes sense to me I think, but I am not sure how to resolve it.
The first error is now fixed in 2.0.3
Thank you!
I ended up doing an assertion for the second error:
this.patch({
"deleted_at": value
} as PartialModelObject<T>);
Not the worst compromise to me...
That error makes complete sense. There is no deleted_at property. You can define it for your model, then you shouldn't need the cast
@koskimas Yes, you are right. I got mixed up since I simplified it for the example. In the code I am using, that column is dynamic based on options passed to the decorator. And I have yet to find a way to dynamically add properties to classes in that way via a decorator.
Fyi @koskimas, generating declaration files ("declaration": true in tsconfig.json) with the examples discussed here does not work, because CustomQueryBuilder must be exported. I've gone into further detail here if you want to give your opinion/insight: https://github.com/olavim/objection-cursor/issues/12
Most helpful comment
The old
ModelClassis now replaced withtypeof Modelin most places.