Back to Sequelize

TypeScript

versioned_docs/version-6.x.x/other-topics/typescript.md

latest10.5 KB
Original Source

:::info

We're working hard on making Sequelize a breeze to use in TypeScript. Some parts are still a work in progress. We recommend using sequelize-typescript to bridge the gap until our improvements are ready to be released.

:::

Sequelize provides its own TypeScript definitions.

Please note that only TypeScript >= 4.1 is supported. Our TypeScript support does not follow SemVer. We will support TypeScript releases for at least one year, after which they may be dropped in a SemVer MINOR release.

As Sequelize heavily relies on runtime property assignments, TypeScript won't be very useful out of the box. A decent amount of manual type declarations are needed to make models workable.

Installation

In order to avoid clashes with different Node versions, the typings for Node are not included. You must install @types/node manually.

Usage

Important: You must use declare on your class properties typings to ensure TypeScript does not emit those class properties. See Caveat with Public Class Fields

Sequelize Models accept two generic types to define what the model's Attributes & Creation Attributes are like:

typescript
import { Model, Optional } from 'sequelize';

// We don't recommend doing this. Read on for the new way of declaring Model typings.

type UserAttributes = {
  id: number;
  name: string;
  // other attributes...
};

// we're telling the Model that 'id' is optional
// when creating an instance of the model (such as using Model.create()).
type UserCreationAttributes = Optional<UserAttributes, 'id'>;

class User extends Model<UserAttributes, UserCreationAttributes> {
  declare id: number;
  declare name: string;
  // other attributes...
}

This solution is verbose. Sequelize >=6.14.0 provides new utility types that will drastically reduce the amount of boilerplate necessary: InferAttributes, and InferCreationAttributes. They will extract Attribute typings directly from the Model:

typescript
import { Model, InferAttributes, InferCreationAttributes, CreationOptional } from 'sequelize';

// order of InferAttributes & InferCreationAttributes is important.
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  // 'CreationOptional' is a special type that marks the field as optional
  // when creating an instance of the model (such as using Model.create()).
  declare id: CreationOptional<number>;
  declare name: string;
  // other attributes...
}

Important things to know about InferAttributes & InferCreationAttributes work: They will select all declared properties of the class except:

  • Static fields and methods.
  • Methods (anything whose type is a function).
  • Those whose type uses the branded type NonAttribute.
  • Those excluded by using InferAttributes like this: InferAttributes<User, { omit: 'properties' | 'to' | 'omit' }>.
  • Those declared by the Model superclass (but not intermediary classes!). If one of your attributes shares the same name as one of the properties of Model, change its name. Doing this is likely to cause issues anyway.
  • Getter & setters are not automatically excluded. Set their return / parameter type to NonAttribute, or add them to omit to exclude them.

InferCreationAttributes works the same way as InferAttributes with one exception:Properties typed using the CreationOptional type will be marked as optional. Note that attributes that accept null, or undefined do not need to use CreationOptional:

typescript
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  declare firstName: string;

  // there is no need to use CreationOptional on lastName because nullable attributes
  // are always optional in User.create()
  declare lastName: string | null;
}

// ...

await User.create({
  firstName: 'Zoé',
  // last name omitted, but this is still valid!
});

You only need to use CreationOptional & NonAttribute on class instance fields or getters.

Example of a minimal TypeScript project with strict type-checking for attributes:

import CodeBlock from '@theme/CodeBlock'; import modelInitExample from '!!raw-loader!@site/.sequelize/v6/test/types/typescriptDocs/ModelInit.ts';

<CodeBlock className="language-typescript"> {modelInitExample} </CodeBlock>

The case of Model.init

Model.init requires an attribute configuration for each attribute declared in typings.

Some attributes don't actually need to be passed to Model.init, this is how you can make this static method aware of them:

  • Methods used to define associations (Model.belongsTo, Model.hasMany, etc…) already handle the configuration of the necessary foreign keys attributes. It is not necessary to configure these foreign keys using Model.init. Use the ForeignKey<> branded type to make Model.init aware of the fact that it isn't necessary to configure the foreign key:

    typescript
    import {
      Model,
      InferAttributes,
      InferCreationAttributes,
      DataTypes,
      ForeignKey,
    } from 'sequelize';
    
    class Project extends Model<InferAttributes<Project>, InferCreationAttributes<Project>> {
      id: number;
      userId: ForeignKey<number>;
    }
    
    // this configures the `userId` attribute.
    Project.belongsTo(User);
    
    // therefore, `userId` doesn't need to be specified here.
    Project.init(
      {
        id: {
          type: DataTypes.INTEGER,
          primaryKey: true,
          autoIncrement: true,
        },
      },
      { sequelize },
    );
    
  • Timestamp attributes managed by Sequelize (by default, createdAt, updatedAt, and deletedAt) don't need to be configured using Model.init, unfortunately Model.init has no way of knowing this. We recommend you use the minimum necessary configuration to silence this error:

    typescript
    import { Model, InferAttributes, InferCreationAttributes, DataTypes } from 'sequelize';
    
    class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
      id: number;
      createdAt: Date;
      updatedAt: Date;
    }
    
    User.init(
      {
        id: {
          type: DataTypes.INTEGER,
          primaryKey: true,
          autoIncrement: true,
        },
        // technically, `createdAt` & `updatedAt` are added by Sequelize and don't need to be configured in Model.init
        // but the typings of Model.init do not know this. Add the following to mute the typing error:
        createdAt: DataTypes.DATE,
        updatedAt: DataTypes.DATE,
      },
      { sequelize },
    );
    

Usage without strict types for attributes

The typings for Sequelize v5 allowed you to define models without specifying types for the attributes. This is still possible for backwards compatibility and for cases where you feel strict typing for attributes isn't worth it.

import modelInitNoAttributesExample from '!!raw-loader!@site/.sequelize/v6/test/types/typescriptDocs/ModelInitNoAttributes.ts';

<CodeBlock className="language-typescript"> {modelInitNoAttributesExample} </CodeBlock>

Usage of Sequelize#define

In Sequelize versions before v5, the default way of defining a model involved using Sequelize#define. It's still possible to define models with that, and you can also add typings to these models using interfaces.

import defineExample from '!!raw-loader!@site/.sequelize/v6/test/types/typescriptDocs/Define.ts';

<CodeBlock className="language-typescript"> {defineExample} </CodeBlock>

Utility Types

Requesting a Model Class

ModelStatic is designed to be used to type a Model class.

Here is an example of a utility method that requests a Model Class, and returns the list of primary keys defined in that class:

typescript
import {
  ModelStatic,
  ModelAttributeColumnOptions,
  Model,
  InferAttributes,
  InferCreationAttributes,
  CreationOptional,
} from 'sequelize';

/**
 * Returns the list of attributes that are part of the model's primary key.
 */
export function getPrimaryKeyAttributes(model: ModelStatic<any>): ModelAttributeColumnOptions[] {
  const attributes: ModelAttributeColumnOptions[] = [];

  for (const attribute of Object.values(model.rawAttributes)) {
    if (attribute.primaryKey) {
      attributes.push(attribute);
    }
  }

  return attributes;
}

class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  id: CreationOptional<number>;
}

User.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true,
    },
  },
  { sequelize },
);

const primaryAttributes = getPrimaryKeyAttributes(User);

Getting a Model's attributes

If you need to access the list of attributes of a given model, Attributes<Model> and CreationAttributes<Model> are what you need to use.

They will return the Attributes (and Creation Attributes) of the Model passed as a parameter.

Don't confuse them with InferAttributes and InferCreationAttributes. These two utility types should only ever be used in the definition of a Model to automatically create the list of attributes from the model's public class fields. They only work with class-based model definitions (When using Model.init).

Attributes<Model> and CreationAttributes<Model> will return the list of attributes of any model, no matter how they were created (be it Model.init or Sequelize#define).

Here is an example of a utility function that requests a Model Class, and the name of an attribute ; and returns the corresponding attribute metadata.

typescript
import {
  ModelStatic,
  ModelAttributeColumnOptions,
  Model,
  InferAttributes,
  InferCreationAttributes,
  CreationOptional,
  Attributes,
} from 'sequelize';

export function getAttributeMetadata<M extends Model>(
  model: ModelStatic<M>,
  attributeName: keyof Attributes<M>,
): ModelAttributeColumnOptions {
  const attribute = model.rawAttributes[attributeName];
  if (attribute == null) {
    throw new Error(`Attribute ${attributeName} does not exist on model ${model.name}`);
  }

  return attribute;
}

class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  id: CreationOptional<number>;
}

User.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true,
    },
  },
  { sequelize },
);

const idAttributeMeta = getAttributeMetadata(User, 'id'); // works!

// @ts-expect-error
const nameAttributeMeta = getAttributeMetadata(User, 'name'); // fails because 'name' is not an attribute of User