DEV Community

Discussion on: Using a TypeScript interface to define model properties in Objection.js

Collapse
vsamma profile image
Veiko Samma • Edited on

Hi,

I found a similar solution a few months ago (don't remember the source though). It's especially good as I don't want to use default exports.

Instead of having an identically named interface to extend your original interface, you must export the class first (which extends Model) and then the new interface which extends both your actual interface and the Model class:

// blog-post-model.ts
import { Model } from "objection";
import { BlogPost } from "./blogpost";

export class BlogPostModel extends Model {
  static get tableName() {
    return "blog_posts";
  }
}

export interface BlogPostModel extends BlogPost, Model {}
Enter fullscreen mode Exit fullscreen mode

(Sorry, new user on dev.to, didn't know how to present code and went with JSFiddle.)

But either way, this solution in this article or my approach, both cause problems when you want to start using relatedQuery.

For example:

const result = await BlogPostModel.relatedQuery('images').for(1); would return this error:

Property 'for' does not exist on type 'never'.ts(2339)

Have you come across this problem and if yes, how would you solve it?

Collapse
tylerlwsmith profile image
Tyler Smith Author

Hey Veiko, welcome to DEV!

If you're just trying to use types within the back-end Node.js app, it may be best to define your properties directly on the model. Here's a simplified example from Objection's official TypeScript example project:

import { Model } from 'objection'
import Person from './Person'

export class Movie extends Model {
  // Table name is the only required property.
  static tableName = 'movies'
  id!: number
  name!: string
  actors!: Person[]
}
Enter fullscreen mode Exit fullscreen mode

This should give you types and autocomplete. Objection makes this example project hard to find because it's at the bottom of the homepage with no fanfare.

If you want to share those types with a front-end application, take a look at Hunter's example further up in the comments.

If you want to leave a comment with code on DEV, you can use three backticks (`) followed by the language, the code, then three more to close the code block. It should look like this in your comment box:

```js
console.log("Hello world!");
```
Enter fullscreen mode Exit fullscreen mode

I hope this helps. Cheers!

Collapse
vsamma profile image
Veiko Samma

Hi Tyler!

Thanks for the really helpful and quick response :) I really appreciate it.

So I also posted this question on Objection github and got a response that I can't do both those things I wanted to do. That indeed, for the "relatedQuery" to work, you have to have the properties defined in the Model class.

So I am currently trying to change all my backend logic accordingly, but before I get it all done and can test stuff, my first concern would be this:

How to handle the case when I have multiple interfaces for one entity? An easy example based on your BlogPost here is for read/write, let's say your full BlogPost interface would have a property called "createdDate" but you don't want that to appear in the interface which is used for creating blog posts because you don't want anybody to accidentally enter this value themselves during blog post creation.
Or another example, for user creation you want to have a password field but when you return a user object to the FE, you don't want to pass the password value back (even if it's hashed at that time).
I get that actually in JS you can add more properties to an object compared to its interface anyways and to be perfectly safe not to add any unwanted data to your DB, you should clean the object before that (or just cherry pick the fields from the input object you need) but still, when our FE devs work on their tasks, they only want to work with an interface for entity creation that has all the required fields and not wonder whether they should fill "creationDate" or similar fields on FE themselves.

So would the best solution then be to define such an interface, have it as an input to a service function and then manually create a DB Model object, map all the required fields from input to model and then insert that model object to the DB?

Btw, here's the link to the github issue, Sami Koskimäki suggested me to just create that separate interface and make the model Implement that. This does create some problems with Partial objects which didn't seem to occur with this Hunter's type approach, but then again having a separate interface seems to be a more decoupled approach.

I'll try them both and see which one works better for me.

Thanks for your help!

Thread Thread
tylerlwsmith profile image
Tyler Smith Author

I hope you're able to get it sorted out! Feel free to leave a comment with which one you liked better incase anyone else who is having the same problem stumbles onto this conversation in the future.

Thread Thread
vsamma profile image
Veiko Samma • Edited on

Thanks!
I will.

But first I need to solve some issues. With this "type" solution offered here, I'm immediately running into problems when creating new objects of this new type "Dataset" (based on DatasetModel).

Although it hides most of Model's properties, it still includes QueryBuilderType and when instantiating a new object, this is a required field to add as well. If I don't, I get this error:

Type ... is missing the following properties from type 'ModelObject<DatasetModel>': QueryBuilderType

Edit:

And when I have an example like this:

//project.ts
export class ProjectModel extends Model {
  public id!: number;
  public name!: string;
  public datasets!: DatasetModel[];

  public static tableName = 'projects';

  static get relationMappings() {
    return {
      datasets: {
        relation: Model.HasManyRelation,
        modelClass: DatasetModel,
        join: {
          from: 'projects.id',
          to: 'datasets.project_id',
        },
      }
    };
  }
}

export type Project = ModelObject<ProjectModel>;


//dataset.ts
export class DatasetModel extends Model {
  public id!: number;
  public projectId!: number;

  public static tableName = 'datasets';

  static get relationMappings() {
    return {
      project: {
        relation: Model.BelongsToOneRelation,
        modelClass: ProjectModel,
        join: {
          from: 'datasets.project_id',
          to: 'projects.id',
        },
      },
  }
}

export type Dataset = ModelObject<DatasetModel>;
Enter fullscreen mode Exit fullscreen mode

I'm running into a problem when I want to instantiate a Project object. I need the "datasets" property to be defined for the Project so I can use it on the object (with autocomplete etc), only relationMapping is not enough. But when its type is DatasetModel (which is the only way relatedQuery works), then I also need to explicitly set the ~30 properties that the Model class has as well. Not to mention the fact that then you need to use the DatasetModel class in your business logical (service) layer which means that the DB communication (ORM) implementational details are not decoupled from the rest of the app anymore.

If I use Hunter's solution with the Type and set the "datasets" property to type "Dataset" for the Project class, then I only need to set the value to 1 extra property (QueryBuilderType, as stated above), but this again breaks the usage of relatedQuery and I'm back to square one.

So I think now I have to start trying with the interface "implements" approach.

But it's so crazy how you can't easily define DB model entities, which have other entities as related properties, all based on interface contracts in Typescript and also use all the query functionality properly.