sequelize: Sequelize with TypeScript - class fields undefined

What are you doing?

I’am using Babel for code compilation + TypeScript for type-checking. Two scenarios:

  1. Using define, which is “really wouldn’t recommend this” (according types/test/define.ts comments)
interface User extends Model {
	id: number;
	login: string;
}

type UserModel = {
	new (): User;
} & typeof Model;

const User = sequelize.define(
	'User',
	{ login: DataTypes.STRING	},
	{ tableName: 'user' }
) as UserModel;
  1. Using new approach (based on code, found in the types/models/User.ts)
export class User extends Model {
	public id: number;
	public login: string;
}

User = User.init(
	{ login: DataTypes.STRING },
	{	sequelize, tableName: 'user' }
);

In DB (Postgres) I have one row with login ‘admin’.

For the test, I use this function.

async function test() {
	const user = (await User.findOne()) as User;
	console.log('login', user.login, user.get('login'));
}
test();

What do you expect to happen?

It is logical that in both scenarios I should get

login admin admin

What is actually happening?

However, if in “modern”, second approach I see

login undefined admin

And if I print console.log(user) I see

user1 User {
  dataValues:
   { id: 1,
     login: 'admin',
     createdAt: 2019-03-18T23:00:33.516Z,
     updatedAt: 2019-03-18T23:00:33.516Z },
  _previousDataValues:
   { id: 1,
     login: 'admin',
     createdAt: 2019-03-18T23:00:33.516Z,
     updatedAt: 2019-03-18T23:00:33.516Z },
  _changed: {},
  _modelOptions:
   { timestamps: true,
     validate: {},
     freezeTableName: false,
     underscored: false,
     paranoid: false,
     rejectOnEmpty: false,
     whereCollection: null,
     schema: null,
     schemaDelimiter: '',
     defaultScope: {},
     scopes: {},
     indexes: [],
     name: { plural: 'Users', singular: 'User' },
     omitNull: false,
     sequelize:
      Sequelize {
        options: [Object],
        config: [Object],
        dialect: [PostgresDialect],
        queryInterface: [QueryInterface],
        models: [Object],
        modelManager: [ModelManager],
        connectionManager: [ConnectionManager],
        importCache: {},
        test: [Object] },
     tableName: 'user',
     hooks: {} },
  _options:
   { isNewRecord: false,
     _schema: null,
     _schemaDelimiter: '',
     raw: true,
     attributes: [ 'id', 'login', 'createdAt', 'updatedAt' ] },
  isNewRecord: false,
  id: undefined,
  login: undefined }

Dialect: postgres “sequelize”: “^5.1.0”,

upd: Just tested with ts-node - everything works as it should. Here is my .babelrc

{
	"sourceMaps": "inline",
	"retainLines": true,

	"presets": [
		[
			"@babel/preset-env",
			{
				"targets": {
					"node": true
				}
			}
		],
		"@babel/preset-typescript"
	],
	"plugins": [
		"@babel/plugin-proposal-optional-chaining",
		"@babel/plugin-proposal-object-rest-spread",
		"@babel/proposal-class-properties"
	]
}

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 4
  • Comments: 41 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Please reopen

The solution for me was to set "target": "ES2020" in tsnconfig.json (it was set to ESNEXT)

Closed huh? Reopen this.

This is still an issue for me. TypeScript v4.0.3 and Sequelize v6.3.5 in a Next.js v9.5.3 project using Node v14.6.0

const created = await User.create({
   id: '1',
   firstname: 'Darth'
});

console.info(created.id, created.firstname); // values are undefined

console.info(created.getDataValue('id'), created.getDataValue('firstname') // outputs correct values

Model Definition:

export interface UserAttributes {
    id: string;
    firstname: string;

    createdAt?: Date;
    updatedAt?: Date;
}

export interface UserCreationAttributes extends Optional<UserAttributes, 'id'> {
}

export class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
    public id: string;
    public firstname: string;

    // timestamps!
    public readonly createdAt!: Date;
    public readonly updatedAt!: Date;
}

User.init(
    {
        id: {
            type: DataTypes.UUIDV4,
            primaryKey: true,
        },
        firstname: {
            type: DataTypes.STRING(36),
            allowNull: true
        },

        createdAt: {
            type: DataTypes.DATE,
            allowNull: false,
            defaultValue: DataTypes.NOW,
        },
        updatedAt: {
            type: DataTypes.DATE,
            allowNull: false,
            defaultValue: DataTypes.NOW,
        }
    },
    {
        sequelize,
    }
);

Ran into this issue today, hacking the constructor would’ve been too ugly, and also would’ve differed from vanilla JS behavior, so I created a little babel plugin to do this job:

Babel plugin to remove relevant attributes before they hit the transpiler
function babelPluginModels() {
    return {
        visitor: {
            ClassDeclaration(path) {
                const { superClass, superTypeParameters, implements } = path.node

                // Assert class is extended, and parent class name is  "Model"
                if (superClass == null || superClass.name !== 'Model') {
                    return
                }

                // Assert class has two type parameters
                if (
                    superTypeParameters == null ||
                    superTypeParameters.type !== 'TSTypeParameterInstantiation' ||
                    superTypeParameters.params.length !== 2
                ) {
                    return
                }

                // Assert class implements an interface
                if (implements == null || implements.length !== 1) {
                    return
                }

                // Remove all relevant attributes from definition
                if (path.node.body != null) {
                    path.node.body.body = path.node.body.body.filter(bodyItem => {
                        // Remove public class properties with no values
                        if (
                            bodyItem.type === 'ClassProperty' &&
                            bodyItem.accessibility === 'public' &&
                            !bodyItem.computed &&
                            bodyItem.value == null
                        ) {
                            return false
                        }
                        return true
                    })
                }
            },
        },
    }
}

module.exports = babelPluginModels
same plugin but with a hard boundary of directory defined
const path = require('path')

const MODELS_DIR = path.resolve(__dirname, '../src/models') + path.sep

function babelPluginModels() {
    return {
        visitor: {
            ClassDeclaration(path) {
                let sourceFilePath = null

                if (
                    path.hub.file != null &&
                    path.hub.file.opts != null &&
                    path.hub.file.opts.filename != null
                ) {
                    sourceFilePath = path.hub.file.opts.filename
                }
                if (sourceFilePath != null && !sourceFilePath.startsWith(MODELS_DIR)) {
                    // Ignore files outside models directory
                    return
                }

                const { superClass, superTypeParameters, implements } = path.node

                // Assert class is extended, and parent class name is  "Model"
                if (superClass == null || superClass.name !== 'Model') {
                    return
                }

                // Assert class has two type parameters
                if (
                    superTypeParameters == null ||
                    superTypeParameters.type !== 'TSTypeParameterInstantiation' ||
                    superTypeParameters.params.length !== 2
                ) {
                    return
                }

                // Assert class implements an interface
                if (implements == null || implements.length !== 1) {
                    return
                }

                // Remove all relevant attributes from definition
                if (path.node.body != null) {
                    path.node.body.body = path.node.body.body.filter(bodyItem => {
                        // Remove public class properties with no values
                        if (
                            bodyItem.type === 'ClassProperty' &&
                            bodyItem.accessibility === 'public' &&
                            !bodyItem.computed &&
                            bodyItem.value == null
                        ) {
                            return false
                        }
                        return true
                    })
                }
            },
        },
    }
}

module.exports = babelPluginModels

It works for me with Next.js default babel preset configured. Here’s what my babel.config.js looks like

{
    "presets": ["next/babel"],
    "plugins": ["./.tooling/babel-plugin-models.js"]
}

Experiencing the same issue. Please reopen.

@Betree 's solution is nice, but doesn’t work with associations. Update this solution for associations:

import { Model } from 'sequelize';

// according to https://github.com/sequelize/sequelize/issues/10579#issuecomment-574604414
// and https://github.com/RobinBuschmann/sequelize-typescript/issues/612#issuecomment-583728166
export default function restoreSequelizeAttributesOnClass(newTarget, self: Model): void {
  [...Object.keys(newTarget.rawAttributes), ...Object.keys(newTarget.associations)].forEach(
    (propertyKey: keyof Model) => {
      Object.defineProperty(self, propertyKey, {
        get() {
          return self.getDataValue(propertyKey);
        },
        set(value) {
          self.setDataValue(propertyKey, value);
        },
      });
    },
  );
}

then,

export class Example extends Model {
  public id: number;
  public name: string;
  public createdAt: Date;
  public updatedAt: Date;

  constructor(...args) {
    super(...args);
    restoreSequelizeAttributesOnClass(new.target, this);
  }
}

Please reopen, I still don’t see any normal solution

If anyone stumbles upon this issue on a new project, I managed to get around it by:

  1. Don’t initialize any of your class properties:
public quantity!: number | null // Good

public quantity!: number | null = null // BAD
  1. Use node v12, to natively support class properties
  2. Correctly order your babel presets:
{ 
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
}

@aaahrens If I remember correctly, a solution was to do it this way:

export class User extends Model {
	public id: number = this.id;
	public login: string = this.login;
}

Even though it’s not pretty.

The only other solution I see, is remove the babel-plugin-proposal-class-properties and upgrade to node 12 which supports class properties natively 😕

Here is the generated code by babel with the typescript preset:

class OfficeModel extends _sequelize.Model {
  constructor(...args) {
    super(...args);

    _defineProperty(this, "id", void 0);

    _defineProperty(this, "field1", void 0);

    _defineProperty(this, "field2", void 0);

    _defineProperty(this, "createdAt", void 0);

    _defineProperty(this, "updatedAt", void 0);
  }

}

I believe the problem is that the super call sets the fields, but then they’re setted to undefined by the _defineProperty function 😕

@SimonSchick It might be useful to call the _initValues from the child class?

Same issue with raw Typescript example starting version 4.3, because now tsc generates empty fields

class User extends sequelize_1.Model {
  id
  name
  preferredName
  createdAt
  updatedAt
  getProjects
  addProject
  hasProject
  countProjects
  createProject
  projects
  static associations
}

instead of

class User extends sequelize_1.Model {}

I have the same issue, would love to see an even more elegant solution

For those who are still scrumbling and asking why @wlchn answer is not working with associations - here you are:

export function restoreSequelizeAttributesOnClass(self: Model, newTarget, values, options): void {
  self._initValues(values, options || {});
  Object.keys(self.constructor.associations).forEach(key => {
    const association = self.constructor.associations[key]
    Object.values(association.accessors).forEach(accessor => {
      delete self[accessor]
    })
    association.mixin(self.constructor.prototype)
  })
}

And in constructor:

constructor(values?: IUserModel, options?: BuildOptions) {
  super(values, options);
  restoreSequelizeAttributesOnClass(this, new.target, values, options);
}

Also this should not be closed, and the code behind Sequelize Mixins should be refactored

Same problem here in similar setup (Typescript 4, Sequelize 6, NextJS 9.5.5 & Node 14.15.1). “@babel/plugin-proposal-class-properties” is required by next.js, seems to cause this bug.

So the cleanest solution I’ve come up with so far is based on a hack proposed by @RobinBuschmann in https://github.com/RobinBuschmann/sequelize-typescript/issues/612#issuecomment-491890977.

The helper:

import { Model } from 'sequelize';

/**
 * Because of https://github.com/sequelize/sequelize/issues/10579, the classes transpiled
 * by Babel are resetting the sequelize properties getters & setters. This helper fixes these properties
 * by re-defining them. Based on https://github.com/RobinBuschmann/sequelize-typescript/issues/612#issuecomment-491890977.
 *
 * Must be called from the constructor.
 */
export default function restoreSequelizeAttributesOnClass(newTarget, self: Model): void {
  Object.keys(newTarget.rawAttributes).forEach((propertyKey: keyof Model) => {
    Object.defineProperty(self, propertyKey, {
      get() {
        return self.getDataValue(propertyKey);
      },
      set(value) {
        self.setDataValue(propertyKey, value);
      },
    });
  });
}

Then in each class, add a special constructor:

export class ExpenseAttachment extends Model<ExpenseAttachment> {
  public id: number;
  public amount: number;
  public url: string;
  public description: string;
  public createdAt: Date;
  public updatedAt: Date;
  public deletedAt: Date;
  public incurredAt: Date;
  public ExpenseId: number;
  public CreatedByUserId: number;

  constructor(...args) {
    super(...args);
    restoreSequelizeAttributesOnClass(new.target, this);
  }
}

This is not perfect, I’d love to see a better solution if anyone has one.

It’s a problem generated by the babel-plugin-proposal-class-properties. Do you know any what to fix this @SimonSchick? Maybe we can call a setup method from the constructor.

I have the same problem when use class definition. I resolve this issue by change to the default way of defining a model. It older but works. I have tested for feature Transaction and Association still work. this is my solution.

Nextjs version: 11.1.2 Sequelize: 6.6.5 Typescript: 4.4.4

import sequelize from "db/connection";
import { DataTypes, Model, Optional } from "sequelize";

/* ------ All Attributes Types -----*/
interface RewardAttributes {
  id: number;
  title: string;
  description: string;
}

/* ------ Specify Optional Attributes Types ----------*/
interface OptionalRewardAttributes extends Optional<RewardAttributes, "id"> {}

/* ------ Declare an interface for our model that is basically what our class would be ------*/
interface RewardBaseType
  extends Model<RewardAttributes, OptionalRewardAttributes>,
    RewardAttributes {}
// Or you can pass the same type to all generic props like this
// interface RewardBaseType extends Model<RewardAttributes, RewardAttributes>, RewardAttributes {}

const RewardModel = sequelize.define<RewardBaseType>(
  "rewards",
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true,
    },
    title: {
      type: DataTypes.STRING(200),
      allowNull: false,
    },
    description: {
      type: DataTypes.TEXT,
      allowNull: true,
    },
  },
  {
    freezeTableName: true,
    timestamps: true,
    createdAt: "created_at",
    updatedAt: "updated_at",
  }
);

export default RewardModel;

This is not really a solution, but dropping babel in favor of esbuild fixed this for me, plus, it’s less configuration than babel+webpack.

For me, the solution has been stop using sequelize, and change to MikroORM

@steelbrain THANK YOU! You are a life saver.

For some reason though, for all my models, bodyItem.accessibility was always undefined, but as soon as I removed that condition from the filter under // Remove public class properties with no values, it started working. This is definitely beyond my understanding, so not sure why that was the case for me or whether I’ve broken something else I haven’t realized yet by removing that check.

Also worth mentioning that just adding the plugin won’t cause next to recompile if you’re running the dev server. Nuking the .next directory was the easiest thing for me to do make sure my models were recompiled using the new plugin.

Same problem here, with node > 12 and not using babel-plugin-proposal-class-properties. @Telokis’s solution above works (thanks for sharing it!) but generates a warning Property '...' is used before its initialization..

Have to add // @ts-ignore Property is used before its initialization all over the place, which can’t be a long term solution for us. Will share progress if I find a better one.

@DanielRamosAcosta Thanks for the answer

I’m having the same problem @petrovi4, how did you solved it?