DEV Community

Cover image for Sequelize + TypeScript - What you need to know
Jean Carlos Taveras
Jean Carlos Taveras

Posted on

Sequelize + TypeScript - What you need to know

Hi Dev community 👋🏾!

I just decided that I want to write a post so here I AM. I didn't know what to write about and I landed on the idea of Sequelize + TypeScript my experience working with Sequelize and adding TypeScript to it.

Table of Content

A Little bit of Background

So the idea came from a project that I was managing and the client decided that they wanted the project handed in yesterday (as usual), so the company put together a small group of Jr. Engineers plus me as the Senior/Lead Engineer so I would be in charge of choosing the stack so I took the decision of using:

  • Express/NodeJS (Backend)
  • PostgreSQL (Database)
  • Plain React (Frontend)
  • Sequelize (ORM)

Since any of the Jr. Engineers had any experience developing on TypeScript nor working on a microservices-based architecture I decided that it would be good for us to use plained JS and monolithic architecture since that what they knew at the moment and I didn't want to slow down the project.

Now I hear you asking what does this have to do with Sequelize + TypeScript? Well, everything. The company decided to use the project as a base ground for another project and it's here when we noticed all the limitations and problems that we have with the current setup. Just to name a few:

  1. A lot of duplicated code both in the Backend and the Frontend
  2. Slow dev environment
  3. Weird bugs that cause the project to crash
  4. It was hard to understand what is doing what
  5. Database Models were messy
  6. And more...

So here I'm refactoring the project using TypeScript and setting up a Microservice-based architecture with Event-Based Communication (as it should) while my team is working on bug fixes and waiting for me to be done with the refactoring.

Well enough talking let's dive in and set up Sequelize to use TypeScript.

Setup Sequelize

I'll be using the sequelize-cli tool which generates the Sequelize folder structure and configuration based on the .sequelizerc file which should look like this

const path = require('path');

module.exports = {
  config: path.resolve('.', 'config.js'),
  'models-path': path.resolve('./db/models'),
  'seeders-path': path.resolve('./db/seeders'),
  'migrations-path': path.resolve('./db/migrations')
};
Enter fullscreen mode Exit fullscreen mode

You can now run npx sequelize-cli init and you will have the following project structure

- config.js
- db/
  - migrations/
  - models/
    - index.js
  - seeders/
Enter fullscreen mode Exit fullscreen mode

Your db/models/index.js file will look like this

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');

const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require('../../app/config')[env];
const db = {};

let sequelize;
if (config.url) {
  sequelize = new Sequelize(config.url, config);
} else {
  // another sequelize configuration
}

fs.readdirSync(__dirname)
  .filter(file => {
    return (
      file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'
    );
  })
  .forEach(file => {
    const model = sequelize['import'](path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;
Enter fullscreen mode Exit fullscreen mode

Yeah, a bunch of code, but this essentially eases the use of models and reduces the amount of requires/imports that you'd do in your models in the future.

Pretty cool stuff, but I want to use TypeScript I hear you say. Ok so let's change our first file to TypeScript. Rename the index.js file to index.ts and let's change some things in it

import { Sequelize } from 'sequelize';

const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config.js')[env];

const  sequelize = config.url
  ? new Sequelize(config.url, config)
  : new Sequelize(config.database, config.username, config.password, config);

export { Sequelize, sequelize };

Enter fullscreen mode Exit fullscreen mode

Yup, that's it!

Ok, let me explain why we removed a bunch of code from the file, and the reason is due to TypeScript being TypeScript. I just didn't want to type the db Object that is being generated based on the files inside the models/ directory. So we removed all of that and just exported Sequelize (Class) and sequelize (instance). The sequelize instance has a reference to the database connection so when we create our models we use this instance so we can communicate with the database through the models.

The first file is done and a bunch more to go.

Setup Sequelize Models

If we were to use JS a Sequelize model would look like this.

NOTE: I'll be declaring two example models: Author, and Book.

Let's first see how to add a Sequelize Model using JS. The Author Model would look like the following

// /models/author.js
module.exports = (sequelize, DataTypes) => {
  const Author = sequelize.define(
    'Author',
    {
      id: {
        allowNull: false,
        autoIncrement: false,
        primaryKey: true,
        type: DataTypes.UUID,
        unique: true,
      },
      firstName: {
        allowNull: true,
        type: DataTypes.TEXT,
      },
      lastName: {
        allowNull: false,
        type: DataTypes.TEXT,
      },
      email: {
        allowNull: true,
        type: DataTypes.TEXT,
      },
    },
    {}
  );

  return Author;
};
Enter fullscreen mode Exit fullscreen mode

So let's give the TypeScript treat to the Author model as well. Change the name to author.ts and let's define some interfaces

import { Model, Optional } from 'sequelize';

interface AuthorAttributes {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
};

/*
  We have to declare the AuthorCreationAttributes to
  tell Sequelize and TypeScript that the property id,
  in this case, is optional to be passed at creation time
*/
interface AuthorCreationAttributes
  extends Optional<AuthorAttributes, 'id'> {}

interface AuthorInstance
  extends Model<AuthorAttributes, AuthorCreationAttributes>,
    AuthorAttributes {
      createdAt?: Date;
      updatedAt?: Date;
    }
Enter fullscreen mode Exit fullscreen mode

We need these interfaces to tell TypeScript which properties an author instance has.

Now let's define the Author Model in the TS way

// model/author.ts
import { sequelize } from '.';

// ... instances code

const Author = sequelize.define<AuthorInstance>(
  'Author',
  {
    id: {
      allowNull: false,
      autoIncrement: false,
      primaryKey: true,
      type: DataTypes.UUID,
      unique: true,
    },
    firstName: {
      allowNull: true,
      type: DataTypes.TEXT,
    },
    lastName: {
      allowNull: false,
      type: DataTypes.TEXT,
    },
    email: {
      allowNull: true,
      type: DataTypes.TEXT,
    },
  }
);
Enter fullscreen mode Exit fullscreen mode

Now you should be wondering, why do I have to go through all of that if I'll end up doing the same? Well, if you made a typo on any property of the model define object, TypeScript will yell at you and tell you that the property X is missing, same if you try to add a property that is not defined on the AuthorAttributes interface TypeScript will tell you that property Y is not defined on type AuthorAttributes

Now that we know how to declare a Sequelize model using TypeScript let's define the book.ts model.

// /models/book.ts
import { Model, Optional } from 'sequelize';
import { sequelize } from '.';

interface BookAttributes {
  id: string;
  title: string;
  numberOfPages: number;
  authorId: string;
}

interface BookCreationAttributes
  extends Optional<BookAttributes, 'id'> {}

interface BookInstance
  extends Model<BookAttributes, BookCreationAttributes>,
    BookAttributes {
      createdAt?: Date;
      updatedAt?: Date;
    }

const Book = sequelize.define<BookInstance>(
  'Book',
  {
    id: {
      allowNull: false,
      autoIncrement: false,
      primaryKey: true,
      type: DataTypes.UUID,
      unique: true,
    },
    title: {
      allowNull: true,
      type: DataTypes.TEXT,
    },
    numberOfPages: {
      allowNull: false,
      type: DataTypes.INTEGER,
    },
    authorId: {
      allowNull: true,
      type: DataTypes.UUID,
    },
  }
);

export default Book;
Enter fullscreen mode Exit fullscreen mode

That's it! That's how you define a Sequelize Model using TypeScript.

Adding Model Association

If you want to add associations to the model let's say An Author HAS MANY Books then in JS you'd do something like this

// model/author.js
  Author.associate = models => {
    Author.hasMany(models.Book, {
      foreignKey: 'authorId',
    });
  };
Enter fullscreen mode Exit fullscreen mode
// model/book.js
  Book.associate = models => {
    Book.belongsTo(models.Author, {
      foreignKey: 'id'
    });
  };
Enter fullscreen mode Exit fullscreen mode

So do you remember when I said that the code generated when we ran npx sequelize-cli init will help us when trying to do requires/imports? That time has come!

Sequelize will try to run the associate function if defined and will add the defined associations to the Models.

But we are here for TypeScript not for JS, so how do we do this in our defined models in TypeScript? Let's see how.

// models/author.ts
import Book from './book';

// ... Code Defining the Author Model

Author.hasMany(Book, {
  /*
    You can omit the sourceKey property
    since by default sequelize will use the primary key defined
    in the model - But I like to be explicit 
  */
  sourceKey: 'id',
  foreignKey: 'authorId',
  as: 'books'
});
Enter fullscreen mode Exit fullscreen mode
// models/book.ts
import Author from './author';

// ... Code Defining the Book Model

Book.belongsTo(Author, {
  foreignKey: 'authorId',
  as: 'author'
});
Enter fullscreen mode Exit fullscreen mode

Is that it? I hear you say. Well, if things were that simple in this world I wouldn't be writing this post 😅.

So why wouldn't this work? Sequelize has a weird way to work and if we try to define this association in their respective files (as we do in JS) Sequelize will throw this error:

throw new Error("${source.name}.${_.lowerFirst(Type.name)} called with something that's not a subclass of Sequelize.Model");
      ^
"Error: Book.belongsTo called with something that's not a subclass of Sequelize.Model"
Enter fullscreen mode Exit fullscreen mode

At first, I thought I was missing something or that I didn't type the Author Model as a Sequelize Model but after double-checking everything was good, TypeScript was not complaining that I was trying to call the belongsTo method with something else that's not a Sequelize Model. So I was wondering why is this happening? 🤔

I was reading the Sequelize documentation and posts from around the web and didn't find anything that would work, except someone (sorry didn't find the link to the StackOverflow thread) that suggested that one should put their association in one Model file. I was skeptical about it since I said to myself: how would this work since I'm exporting the same model that is in my file. However, I gave it a try. I moved the belongsTo call to the models/author.ts file:

// model/author.ts
import Book from './book';

// ... a bunch of code here

Book.belongsTo(Author, {
  foreignKey: 'authorId',
  as: 'author'
});
Enter fullscreen mode Exit fullscreen mode

And Lord and behold my project was running:

The library Server is running on http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

It is weird but it seems that you have to define the belongsTo association in the same file that you set the hasMany or the hasOne association.

Conclusion

In the end, setting up Sequelize with TypeScript wasn't hard at all, and now we know which properties belong to which model.

For me, it was a valuable experience since I thought that translating my JS files to TS would just be done by renaming the file and that would be it since I believed that Sequelize would handle the typing work in the background.

Note: I know that there is a package out there called sequelize-typescript but I didn't feel like adding another dependency to my project, plus this package is not maintained by the Sequelize team. However, I just tried it and it's pretty cool, so give it a try if you don't want a setup like mine.

By the end of the day, I don't feel like having another dependency in my project, and I was able to accomplish the goal of adding TypeScript to Sequlize without a third-party library just with pure TS.

Sorry for the long post, but I hope you find it useful if you are trying to use Sequelize and TypeScript together.👋🏾

Top comments (14)

Collapse
 
eeeee profile image
E

Sequelize-typescript package is very helpful, it allows to define model with decorators and transpiles into easily usable objects to pass through graphql, without creating additional boilerplate DTO.
Also, check out Nestjs. It's very flexible, code first, easy to use and
"federationable" typescript based, backend framework.
nestjs.com

Collapse
 
jctaveras profile image
Jean Carlos Taveras • Edited

Totally agree with you! That's why I added the note at the end.

I'm changing a lot of stuff both in the backend and the frontend. It's something kinda like an extreme makeover for the project. 😅

Collapse
 
demostheneslld profile image
Nathaniel Clement

Maybe a bit silly, but I was starting without sequelize installed, and needed to run npm i sequelize before npx sequelize-cli init

Collapse
 
luiz_fernandoreal_8737ba profile image
Luiz Fernando Real

Thanks for the post! It's a good direction for me.

I'd like also to say that the problem you had putting each association in a different file is probably because of the circular reference it creates between the model files. Since you import Author model inside Books and vice-versa, Node needs to give an empty object to one of them so that they can both be loaded. I've done that myself also in the project I'm working on.

Collapse
 
joaopedromatias profile image
João Pedro Matias

How would the sequelize-cli db:migrate command work with typescript?

Collapse
 
alechuu profile image
Alechuu

Just dropping by the comments to say that you saved me! Was having the same problem related to the Sequelize subclass and didn't find anything useful in Google until I came across your post.
Really appreciated!

Collapse
 
hcancelik profile image
Can Celik

Your model files are ending with .ts but db/models/index.js is looking for .js files so this probably doesn't work.

Collapse
 
20101997 profile image
Ah_ch_97 • Edited

I think it will be better to keep the db Object that is being generated based on the files inside the models so you can avoid :
Book.belongsTo called with something that's not a subclass of Sequelize.Model error

Collapse
 
okumujustine profile image
okumujustine

Nice heads up, i love it

Collapse
 
unlocomqx profile image
Mohamed Jbeli

Thank you! This improves DX quite significantly

Collapse
 
minhdc998 profile image
MinhDC998

Awesome post, thank you.
But I wondering how to add or remove column to existed model.

Collapse
 
raitono profile image
Ben Painter

Hey @minhdc998, you should be able to remove the property from your model and then use the sequelize-cli to generate a migration in which you would write a table alter statement to remove it from the database.

There is supposed to be an alter property you can pass to the sync method which might attempt to do this for you, but I've had issues with it creating duplicate foreign key constraints, so I haven't used it much.