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

tylerlwsmith

Tyler Smith

Posted on October 2, 2020

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

I'm working on a full-stack TypeScript project using Next.js, and I'm trying to share TypeScript definitions between Node and React. I'm using Objection.js as an ORM to talk to the database, and it has great built-in TypeScript support.

Ideally, I'd like to define an interface once and use that on both the client and the server. Making this work with Objection proved to be more challenging than I had hoped.

My first attempt

I originally tried making an interface, and then implementing that interface on my Objection.js Model. Here's what that looked like:

// types.d.ts
interface BlogPost {
  id: number;
  title?: string;
  content?: string;
  slug?: string;
}
Enter fullscreen mode Exit fullscreen mode
// blog-post-model.ts
import { Model } from "objection";

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

export default BlogPostModel;
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this doesn't work that well. TypeScript gives the following error:

Class 'BlogPostModel' incorrectly implements interface 'BlogPost'. Property 'id' is missing in type 'BlogPostModel' but required in type 'BlogPost'.ts(2420)

TypeScript is upset because it doesn't see any of the properties that were defined in the interface implemented on the model, and it doesn't know that those properties will be added magically by Objection.

To make this solution work, I would need to define all of my model's properties again on the Objection model. This would mean that every time I change the interface, I'd also have to change the model, even though it's just going to have the exact same properties. Yikes.

A better solution: declaration merging

Among TypeScript's numerous and wonky features is declaration merging. There's a lot to unpack with this feature, so if you want to get a solid understanding of its capabilities then read the docs.

One feature that does not appear to be in the documentation is class/interface merging (found in this GitHub comment). If you have an interface with the same name as a class that is in the same file, class/interface merging will automatically merge the interface properties with the class by the same name.

Here is my revised code that takes advantage of class/interface merging:

// types.d.ts
interface BlogPost {
  id: number;
  title?: string;
  content?: string;
  slug?: string;
}
Enter fullscreen mode Exit fullscreen mode
// blog-post-model.ts
import { Model } from "objection";

interface BlogPostModel extends BlogPost {}

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

export default BlogPostModel;
Enter fullscreen mode Exit fullscreen mode

Because we gave the BlogPostModel interface the same name as the class, it automatically merges the interface properties with the class, and it gives us all of TypeScript's autocomplete goodness without having to redefine interface properties on the Objection model.

Also, take a look at the comments below, there are some good suggested alternatives to this approach. Do you know a better way to do this? If so, let me know in the comments below!

💖 💪 🙅 🚩
tylerlwsmith
Tyler Smith

Posted on October 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related