Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configuring babel-typescript for Sequelize ORM causes undefined properties

There is a problem when using sequelize-typescript together with babel-preset-typescript. Columns should be retrieved through the Object.defineProperty() but when an existing property already exists it is skipped see source here. When running the code with regular typescript implementation with Sequelize† everything works but when implementing babel-typescript the properties remain.

Question: How should I define my babel configuration so that the properties are properly ignored?

Babel definitions (babel.config.js)

I've tried two Babel definitions, one that has all desired features & one that requires explicit definitions.

Full definition

module.exports = {
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        regenerator: true,
      },
    ],
    "babel-plugin-inline-import",
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    ["@babel/proposal-class-properties", { loose: true }],
    "@babel/proposal-object-rest-spread",
    "@babel/plugin-proposal-optional-chaining",
    [
      "module-resolver",
      {
        alias: {
          "~": "./src",
        },
      },
    ],
  ],
  presets: ["@babel/preset-env", "@babel/typescript"],
};

Tiny explicit definition

module.exports = {
  plugins: [
    ["@babel/proposal-decorators", { legacy: true }],
    ["@babel/proposal-class-properties", { loose: true }],
    "@babel/proposal-async-generator-functions",
    "@babel/proposal-object-rest-spread",
    "@babel/plugin-transform-runtime",
  ],
  presets: ["@babel/preset-env", "@babel/typescript"],
};

Background & details

You can find a regular non-babel example for testing here, the branch babel_build and babel_explicit contain different babel-implementations.

The gist of defining a table is a trivial case of adding decorators to the code:

import { Table, Column, Model } from 'sequelize-typescript'

@Table
class Person extends Model {
  @Column
  name: string

  @Column
  birthday: Date
}

The problem is that if we then retrieve a case through:

const p = await Person.findOne({ name: "John Doe" })
console.log(p)

we get:

Person {
 dataValues: {
   id: 1,
   name: "John Doe",
   birthday: null
 },
  _previousDataValues: {
   id: 1,
   name: "John Doe",
   birthday: null
 },
  _changed: Set(0) {},
  _options: {
    isNewRecord: false,
    _schema: null,
    _schemaDelimiter: '',
    raw: true,
    attributes: [
      'id',
      'name',
      'birthday'
    ]
  },
  isNewRecord: false,
  name: undefined,
  birthday: undefined,
}

note the last two rows with undefined.

Update - using declare

It seems that the correct definitions should be using the declare keyword. While this should solve the issue it doesn't work in practice.

import { Table, Column, Model } from 'sequelize-typescript'

@Table
class Person extends Model {
  @Column
  declare name: string

  @Column
  declare birthday: Date
}

with the config:

module.exports = {
  plugins: [
    ["@babel/plugin-transform-typescript", { allowDeclareFields: true }],
    [
      "@babel/plugin-transform-runtime",
      {
        regenerator: true,
      },
    ],
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    ["@babel/proposal-class-properties", { loose: true }],
    [
      "module-resolver",
      {
        alias: {
          "~": "./src",
        },
      },
    ],
  ],
  presets: ["@babel/preset-env"],
};

Unfortunately, the issue is not resolved but seems that declare is more in line with the intended use.

† this is most likely independent of the sequelize-typescript add-ons

like image 309
Max Gordon Avatar asked Mar 15 '26 07:03

Max Gordon


1 Answers

Update 2022-01-21

You should use declare instead of public ....

For example:

class User extends Model<UserAttributes, UserCreationAttributes>
  implements UserAttributes {
  //public id!: number; // Note that the `null assertion` `!` is required in strict mode.
  declare id: number;

  //public name!: string;
  declare name: string;

  //public preferredName!: string | null; // for nullable fields
  declare preferredName: string | null;

  // timestamps!
  //public readonly createdAt!: Date;
  declare readonly createdAt: Date;

Source:

  • https://github.com/sequelize/sequelize/pull/13877/files
  • https://github.com/sequelize/sequelize/issues/11675#issuecomment-1003765448

Original answer

From https://sequelize.org/master/manual/typescript.html, the TypeScript definition for model initialization is correct, but the documentation is not correct!

When you use the null assertion (public id!: number; // Note that the "null assertion" "!" is required in strict mode), this make the attribute as undefined at instance initialization.

My idea is to force the model initialization using getDataValue() at Model Constructor and avoid to use the recommended null assertion.

Example

import {
  BuildOptions,
  Sequelize,
  Model,
  Optional,
} from "sequelize";

const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb");

// These are all the attributes in the User model
interface UserAttributes {
  id: number;
  name: string;
  //preferredName!: string | null;
  preferredName: string | null;
}

// Some attributes are optional in `User.build` and `User.create` calls
interface UserCreationAttributes extends Optional<UserAttributes, "id"> {}

class User extends Model<UserAttributes, UserCreationAttributes>
  implements UserAttributes {
  //public id!: number; // Note that the `null assertion` `!` is required in strict mode.
  public id: number;

  //public name!: string;
  public name: string;

  //public preferredName!: string | null; // for nullable fields
  public preferredName: string | null;

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

  //public readonly updatedAt!: Date;
  public readonly updatedAt: Date;

  /**
   * Initialization to fix Sequelize Issue #11675.
   *
   * @see https://stackoverflow.com/questions/66515762/configuring-babel-typescript-for-sequelize-orm-causes-undefined-properties
   * @see https://github.com/sequelize/sequelize/issues/11675
   * @ref #SEQUELIZE-11675
   */
  constructor(values?: TCreationAttributes, options?: BuildOptions) {
    super(values, options);

    // All fields should be here!
    this.id = this.getDataValue('id');
    this.name = this.getDataValue('name');
    this.preferredName = this.getDataValue('preferredName');
    this.createdAt = this.getDataValue('createdAt');
    this.updatedAt = this.getDataValue('updatedAt');
  }
}
like image 185
Eduardo Cuomo Avatar answered Mar 17 '26 04:03

Eduardo Cuomo