Skip to content

Commit f1099df

Browse files
authored
Typings: allow $relatedQuery to be strongly typed without a cast (#709)
* Use keyof to infer relation types
1 parent e5028a9 commit f1099df

File tree

2 files changed

+35
-10
lines changed

2 files changed

+35
-10
lines changed

tests/ts/examples.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,22 @@ const {lit, raw, ref} = objection;
1313
class Person extends objection.Model {
1414
firstName: string;
1515
lastName: string;
16+
mom: Person;
17+
comments: Comment[]
1618

1719
static columnNameMappers = objection.snakeCaseMappers();
1820

1921
examplePersonMethod = (arg: string) => 1;
2022

21-
async petsWithId(petId: number): Promise<Animal[]> {
22-
// Types can't look at strings and give strong types, so <Animal> needs
23-
// to be provided to $relatedQuery
24-
return await this.$relatedQuery<Animal>('pets').where('id', petId);
23+
// $relatedQuery can either take a cast, if you don't want to add the field
24+
// to your model:
25+
petsWithId(petId: number): Promise<Animal[]> {
26+
return this.$relatedQuery<Animal>('pets').where('id', petId);
27+
}
28+
29+
// Or, if you add the field, this.$relatedQuery just works:
30+
fetchMom(): Promise<Person> {
31+
return this.$relatedQuery('mom')
2532
}
2633

2734
async $beforeInsert(queryContext: objection.QueryContext) {
@@ -109,6 +116,8 @@ async () => {
109116

110117
class Movie extends objection.Model {
111118
title: string;
119+
actors: Person[];
120+
112121
/**
113122
* This static field instructs Objection how to hydrate and persist
114123
* relations. By making relationMappings a thunk, we avoid require loops
@@ -130,6 +139,11 @@ class Movie extends objection.Model {
130139
});
131140
}
132141

142+
async () => {
143+
// Another example of strongly-typed $relatedQuery without a cast:
144+
takesPeople(await new Movie().$relatedQuery("actors"));
145+
}
146+
133147
class Animal extends objection.Model {
134148
species: string;
135149

typings/objection/index.d.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ declare namespace Objection {
106106
json: Pojo;
107107
options: ModelOptions;
108108
}
109-
109+
110110
export class Validator {
111111
beforeValidate(args: ValidatorArgs): void;
112112
validate(args: ValidatorArgs): Pojo;
@@ -128,7 +128,7 @@ declare namespace Objection {
128128
data: any;
129129
message: string;
130130
}
131-
131+
132132
export interface ErrorHash {
133133
[columnName: string]: ValidationError[];
134134
}
@@ -446,17 +446,28 @@ declare namespace Objection {
446446
$query(trxOrKnex?: Transaction | knex): QueryBuilderSingle<this>;
447447

448448
/**
449-
* Users need to explicitly type these calls, as the relationName doesn't
450-
* indicate the type (and if it returned Model directly, Partial<Model>
451-
* guards are worthless)
449+
* If you add model relations as fields, $relatedQuery works
450+
* automatically:
451+
*/
452+
$relatedQuery<K extends keyof this>(
453+
relationName: K,
454+
trxOrKnex?: Transaction | knex
455+
): QueryBuilderSingle<this[K]>;
456+
457+
// PLEASE NOTE: `$relatedQuery<K extends keyof this>` MUST BE DEFINED
458+
// BEFORE `$relatedQuery<M extends Model>`!
459+
460+
/**
461+
* If you don't want to add the fields to your model, you can cast the
462+
* call to the expected Model subclass (`$relatedQuery<Animal>('pets')`).
452463
*/
453464
$relatedQuery<M extends Model>(
454465
relationName: string,
455466
trxOrKnex?: Transaction | knex
456467
): QueryBuilder<M>;
457468

458469
$loadRelated<T>(
459-
expression: RelationExpression,
470+
expression: keyof this | RelationExpression,
460471
filters?: Filters<T>,
461472
trxOrKnex?: Transaction | knex
462473
): QueryBuilderSingle<this>;

0 commit comments

Comments
 (0)