diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 684e9a48c286..cde929b3dee2 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -5,7 +5,8 @@ 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. +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 @@ -16,131 +17,177 @@ In order to avoid installation bloat for non TS users, you must install the foll ## Usage -Example of a minimal TypeScript project with strict type-checking for attributes. - **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](./model-basics.html#caveat-with-public-class-fields) -**NOTE:** Keep the following code in sync with `/types/test/typescriptDocs/ModelInit.ts` to ensure it typechecks correctly. +Sequelize Models accept two generic types to define what the model's Attributes & Creation Attributes are like: ```typescript -/** - * Keep this file in sync with the code in the "Usage" section in typescript.md - */ -import { - Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize -} from "sequelize"; +import { Model, Optional } from 'sequelize'; -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); +// We don't recommend doing this. Read on for the new way of declaring Model typings. -// These are all the attributes in the User model -interface UserAttributes { - id: number; - name: string; - preferredName: string | null; +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; + +class User extends Model { + declare id: number; + declare string: number; + // other attributes... } +``` -// Some attributes are optional in `User.build` and `User.create` calls -interface UserCreationAttributes extends Optional {} +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, InferCreationAttributes> { + // '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; + declare string: number; + // 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 AttributesOf like this: `InferAttributes`. +- 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 `AttributesOf` with one exception: Properties typed using the `CreationOptional` type +will be marked as optional. + +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: -class User extends Model - implements UserAttributes { - declare id: number; // Note that the `null assertion` `!` is required in strict mode. +[//]: # (NOTE for maintainers: Keep the following code in sync with `/types/test/typescriptDocs/ModelInit.ts` to ensure it typechecks correctly.) + +```typescript +import { + Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, + HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, + Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute +} from 'sequelize'; + +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); + +// 'projects' is excluded as it's not an attribute, it's an association. +class User extends Model, InferCreationAttributes> { + // id can be undefined during creation when using `autoIncrement` + declare id: CreationOptional; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! - declare readonly createdAt: Date; - declare readonly updatedAt: Date; + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; // Since TS cannot determine model association at compile time // we have to declare them here purely virtually // these will not exist until `Model.init` was called. declare getProjects: HasManyGetAssociationsMixin; // Note the null assertions! declare addProject: HasManyAddAssociationMixin; + declare addProjects: HasManyAddAssociationsMixin; + declare setProjects: HasManySetAssociationsMixin; + declare removeProject: HasManyRemoveAssociationMixin; + declare removeProjects: HasManyRemoveAssociationsMixin; declare hasProject: HasManyHasAssociationMixin; + declare hasProjects: HasManyHasAssociationsMixin; declare countProjects: HasManyCountAssociationsMixin; - declare createProject: HasManyCreateAssociationMixin; + declare createProject: HasManyCreateAssociationMixin; // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + declare projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + + // getters that are not attributes should be tagged using NonAttribute + // to remove them from the model's Attribute Typings. + get fullName(): NonAttribute { + return this.name; + } declare static associations: { projects: Association; }; } -interface ProjectAttributes { - id: number; - ownerId: number; - name: string; - description?: string; -} - -interface ProjectCreationAttributes extends Optional {} - -class Project extends Model - implements ProjectAttributes { - declare id: number; +class Project extends Model< + InferAttributes, + InferCreationAttributes +> { + // id can be undefined during creation when using `autoIncrement` + declare id: CreationOptional; declare ownerId: number; declare name: string; - declare readonly createdAt: Date; - declare readonly updatedAt: Date; -} + // `owner` is an eagerly-loaded association. + // We tag it as `NonAttribute` + declare owner?: NonAttribute; -interface AddressAttributes { - userId: number; - address: string; + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; } -// You can write `extends Model` instead, -// but that will do the exact same thing as below -class Address extends Model implements AddressAttributes { +class Address extends Model< + InferAttributes
, + InferCreationAttributes
+> { declare userId: number; declare address: string; - declare readonly createdAt: Date; - declare readonly updatedAt: Date; + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; } -// You can also define modules in a functional way -interface NoteAttributes { - id: number; - title: string; - content: string; -} - -// You can also set multiple attributes optional at once -interface NoteCreationAttributes - extends Optional {} - Project.init( { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, ownerId: { type: DataTypes.INTEGER.UNSIGNED, - allowNull: false, + allowNull: false }, name: { type: new DataTypes.STRING(128), - allowNull: false, - }, - description: { - type: new DataTypes.STRING(128), - allowNull: true, + allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { sequelize, - tableName: "projects", + tableName: 'projects' } ); @@ -149,90 +196,103 @@ User.init( id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, name: { type: new DataTypes.STRING(128), - allowNull: false, + allowNull: false }, preferredName: { type: new DataTypes.STRING(128), - allowNull: true, + allowNull: true }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { - tableName: "users", - sequelize, // passing the `sequelize` instance is required + tableName: 'users', + sequelize // passing the `sequelize` instance is required } ); Address.init( { userId: { - type: DataTypes.INTEGER.UNSIGNED, + type: DataTypes.INTEGER.UNSIGNED }, address: { type: new DataTypes.STRING(128), - allowNull: false, + allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { - tableName: "address", - sequelize, // passing the `sequelize` instance is required + tableName: 'address', + sequelize // passing the `sequelize` instance is required } ); +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +type NoteCreationAttributes = Optional; + // And with a functional approach defining a module looks like this const Note: ModelDefined< NoteAttributes, NoteCreationAttributes > = sequelize.define( - "Note", + 'Note', { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, title: { type: new DataTypes.STRING(64), - defaultValue: "Unnamed Note", + defaultValue: 'Unnamed Note' }, content: { type: new DataTypes.STRING(4096), - allowNull: false, - }, + allowNull: false + } }, { - tableName: "notes", + tableName: 'notes' } ); // Here we associate which actually populates out pre-declared `association` static and other methods. User.hasMany(Project, { - sourceKey: "id", - foreignKey: "ownerId", - as: "projects", // this determines the name in `associations`! + sourceKey: 'id', + foreignKey: 'ownerId', + as: 'projects' // this determines the name in `associations`! }); -Address.belongsTo(User, { targetKey: "id" }); -User.hasOne(Address, { sourceKey: "id" }); +Address.belongsTo(User, { targetKey: 'id' }); +User.hasOne(Address, { sourceKey: 'id' }); async function doStuffWithUser() { const newUser = await User.create({ - name: "Johnny", - preferredName: "John", + name: 'Johnny', + preferredName: 'John', }); console.log(newUser.id, newUser.name, newUser.preferredName); const project = await newUser.createProject({ - name: "first!", - ownerId: 123, + name: 'first!' }); const ourUser = await User.findByPk(1, { include: [User.associations.projects], - rejectOnEmpty: true, // Specifying true here removes `null` from the return type! + rejectOnEmpty: true // Specifying true here removes `null` from the return type! }); // Note the `!` null assertion since TS can't know if we included @@ -250,8 +310,7 @@ async function doStuffWithUser() { 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. -**NOTE:** Keep the following code in sync with `typescriptDocs/ModelInitNoAttributes.ts` to ensure -it typechecks correctly. +[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/ModelInitNoAttributes.ts` to ensure it typechecks correctly.) ```ts import { Sequelize, Model, DataTypes } from "sequelize"; @@ -259,9 +318,9 @@ import { Sequelize, Model, DataTypes } from "sequelize"; const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); class User extends Model { - public id!: number; // Note that the `null assertion` `!` is required in strict mode. - public name!: string; - public preferredName!: string | null; // for nullable fields + declare id: number; + declare name: string; + declare preferredName: string | null; } User.init( @@ -303,8 +362,7 @@ async function doStuffWithUserModel() { 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. -**NOTE:** Keep the following code in sync with `typescriptDocs/Define.ts` to ensure -it typechecks correctly. +[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/Define.ts` to ensure it typechecks correctly.) ```ts import { Sequelize, Model, DataTypes, Optional } from "sequelize"; @@ -345,8 +403,7 @@ async function doStuff() { If you're comfortable with somewhat less strict typing for the attributes on a model, you can save some code by defining the Instance to just extend `Model` without any attributes in the generic types. -**NOTE:** Keep the following code in sync with `typescriptDocs/DefineNoAttributes.ts` to ensure -it typechecks correctly. +[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/DefineNoAttributes.ts` to ensure it typechecks correctly.) ```ts import { Sequelize, Model, DataTypes } from "sequelize"; @@ -376,3 +433,97 @@ async function doStuff() { console.log(instance.id); } ``` + +## 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): ModelAttributeColumnOptions[] { + const attributes: ModelAttributeColumnOptions[] = []; + + for (const attribute of Object.values(model.rawAttributes)) { + if (attribute.primaryKey) { + attributes.push(attribute); + } + } + + return attributes; +} + +class User extends Model, InferCreationAttributes> { + id: CreationOptional; +} + +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` and `CreationAttributes` +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 every 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` and `CreationAttributes` 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(model: ModelStatic, attributeName: keyof Attributes): 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, InferCreationAttributes> { + id: CreationOptional; +} + +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 +``` diff --git a/types/lib/associations/belongs-to-many.d.ts b/types/lib/associations/belongs-to-many.d.ts index b7106ffb197b..af3e3075cc86 100644 --- a/types/lib/associations/belongs-to-many.d.ts +++ b/types/lib/associations/belongs-to-many.d.ts @@ -1,6 +1,7 @@ import { BulkCreateOptions, CreateOptions, + CreationAttributes, Filterable, FindAttributeOptions, FindOptions, @@ -9,7 +10,7 @@ import { Model, ModelCtor, ModelType, - Transactionable + Transactionable, } from '../model'; import { Association, AssociationScope, ForeignKeyOptions, ManyToManyOptions, MultiAssociationAccessors } from './base'; @@ -303,7 +304,7 @@ export interface BelongsToManyCreateAssociationMixinOptions extends CreateOption * @see Instance */ export type BelongsToManyCreateAssociationMixin = ( - values?: TModel['_creationAttributes'], + values?: CreationAttributes, options?: BelongsToManyCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/associations/belongs-to.d.ts b/types/lib/associations/belongs-to.d.ts index fd2a5e356b2a..19d50859290e 100644 --- a/types/lib/associations/belongs-to.d.ts +++ b/types/lib/associations/belongs-to.d.ts @@ -1,5 +1,5 @@ import { DataType } from '../data-types'; -import { CreateOptions, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; +import { CreateOptions, CreationAttributes, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; import { Association, AssociationOptions, SingleAssociationAccessors } from './base'; // type ModelCtor = InstanceType; @@ -117,7 +117,7 @@ export interface BelongsToCreateAssociationMixinOptions * @see Instance */ export type BelongsToCreateAssociationMixin = ( - values?: TModel['_creationAttributes'], + values?: CreationAttributes, options?: BelongsToCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/associations/has-many.d.ts b/types/lib/associations/has-many.d.ts index b1780aa60b4a..ee83127b845a 100644 --- a/types/lib/associations/has-many.d.ts +++ b/types/lib/associations/has-many.d.ts @@ -1,12 +1,13 @@ import { DataType } from '../data-types'; import { CreateOptions, + CreationAttributes, Filterable, FindOptions, InstanceUpdateOptions, Model, ModelCtor, - Transactionable + Transactionable, } from '../model'; import { Association, ManyToManyOptions, MultiAssociationAccessors } from './base'; @@ -211,10 +212,10 @@ export interface HasManyCreateAssociationMixinOptions extends CreateOptions */ export type HasManyCreateAssociationMixin< TModel extends Model, - TForeignKey extends keyof TModel['_creationAttributes'] = never, - TScope extends keyof TModel['_creationAttributes'] = never + TForeignKey extends keyof CreationAttributes = never, + TScope extends keyof CreationAttributes = never > = ( - values?: Omit, + values?: Omit, TForeignKey | TScope>, options?: HasManyCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/associations/has-one.d.ts b/types/lib/associations/has-one.d.ts index e81784b3d88a..692b9e1efcbf 100644 --- a/types/lib/associations/has-one.d.ts +++ b/types/lib/associations/has-one.d.ts @@ -1,5 +1,5 @@ import { DataType } from '../data-types'; -import { CreateOptions, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; +import { CreateOptions, CreationAttributes, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; import { Association, AssociationOptions, SingleAssociationAccessors } from './base'; /** @@ -114,6 +114,6 @@ export interface HasOneCreateAssociationMixinOptions extends HasOneSetAssociatio * @see Instance */ export type HasOneCreateAssociationMixin = ( - values?: TModel['_creationAttributes'], + values?: CreationAttributes, options?: HasOneCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/hooks.d.ts b/types/lib/hooks.d.ts index f62f7f61573e..b9620f172a64 100644 --- a/types/lib/hooks.d.ts +++ b/types/lib/hooks.d.ts @@ -1,4 +1,4 @@ -import { ModelType } from '../index'; +import { Attributes, CreationAttributes, ModelType } from '../index'; import { ValidationOptions } from './instance-validator'; import Model, { BulkCreateOptions, @@ -97,13 +97,19 @@ export class Hooks< /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use Attributes instead of this property to be forward-compatible. */ - _attributes: TModelAttributes; + _attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use CreationAttributes instead of this property to be forward-compatible. */ - _creationAttributes: TCreationAttributes; + _creationAttributes: TCreationAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) /** * Add a hook to the model @@ -113,20 +119,20 @@ export class Hooks< */ public static addHook< H extends Hooks, - K extends keyof SequelizeHooks + K extends keyof SequelizeHooks, CreationAttributes> >( this: HooksStatic, hookType: K, name: string, - fn: SequelizeHooks[K] + fn: SequelizeHooks, CreationAttributes>[K] ): HooksCtor; public static addHook< H extends Hooks, - K extends keyof SequelizeHooks + K extends keyof SequelizeHooks, CreationAttributes> >( this: HooksStatic, hookType: K, - fn: SequelizeHooks[K] + fn: SequelizeHooks, CreationAttributes>[K] ): HooksCtor; /** @@ -134,7 +140,7 @@ export class Hooks< */ public static removeHook( this: HooksStatic, - hookType: keyof SequelizeHooks, + hookType: keyof SequelizeHooks, CreationAttributes>, name: string, ): HooksCtor; @@ -143,11 +149,11 @@ export class Hooks< */ public static hasHook( this: HooksStatic, - hookType: keyof SequelizeHooks, + hookType: keyof SequelizeHooks, CreationAttributes>, ): boolean; public static hasHooks( this: HooksStatic, - hookType: keyof SequelizeHooks, + hookType: keyof SequelizeHooks, CreationAttributes>, ): boolean; /** diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 5589662ca1ce..b5f5982dbf92 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -6,8 +6,8 @@ import { HookReturn, Hooks, ModelHooks } from './hooks'; import { ValidationOptions } from './instance-validator'; import { IndexesOptions, QueryOptions, TableName } from './query-interface'; import { Sequelize, SyncOptions } from './sequelize'; +import { Col, Fn, Literal, Where, MakeUndefinedOptional, AnyFunction } from './utils'; import { LOCK, Transaction, Op } from '..'; -import { Col, Fn, Literal, Where } from './utils'; import { SetRequired } from '../type-helpers/set-required' export interface Logging { @@ -1403,13 +1403,13 @@ export interface ModelOptions { * Define the default search scope to use for this model. Scopes have the same form as the options passed to * find / findAll. */ - defaultScope?: FindOptions; + defaultScope?: FindOptions>; /** * More scopes, defined in the same way as defaultScope above. See `Model.scope` for more information about * how scopes are defined, and what you can do with them */ - scopes?: ModelScopeOptions; + scopes?: ModelScopeOptions>; /** * Don't persits null values. This means that all columns with null values will not be saved. @@ -1509,7 +1509,7 @@ export interface ModelOptions { * See Hooks for more information about hook * functions and their signatures. Each property can either be a function, or an array of functions. */ - hooks?: Partial>; + hooks?: Partial>>; /** * An object of model wide validations. Validations have access to all model values via `this`. If the @@ -1576,13 +1576,20 @@ export abstract class Model * ): Promise; * ``` + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use Attributes instead of this property to be forward-compatible. */ - _attributes: TModelAttributes; + _attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in hooks.d.ts) + /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use CreationAttributes instead of this property to be forward-compatible. */ - _creationAttributes: TCreationAttributes; + _creationAttributes: TCreationAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in hooks.d.ts) /** The name of the database table */ public static readonly tableName: string; @@ -1669,7 +1676,7 @@ export abstract class Model, M extends InstanceType>( this: MS, - attributes: ModelAttributes, options: InitOptions + attributes: ModelAttributes>, options: InitOptions ): MS; /** @@ -1787,13 +1794,13 @@ export abstract class Model( this: ModelStatic, name: string, - scope: FindOptions, + scope: FindOptions>, options?: AddScopeOptions ): void; public static addScope( this: ModelStatic, name: string, - scope: (...args: readonly any[]) => FindOptions, + scope: (...args: readonly any[]) => FindOptions>, options?: AddScopeOptions ): void; @@ -1861,7 +1868,7 @@ export abstract class Model( this: ModelStatic, - options?: FindOptions): Promise; + options?: FindOptions>): Promise; /** * Search for a single instance by its primary key. This applies LIMIT 1, so the listener will @@ -1870,12 +1877,12 @@ export abstract class Model( this: ModelStatic, identifier: Identifier, - options: Omit, 'where'> + options: Omit>, 'where'> ): Promise; public static findByPk( this: ModelStatic, identifier?: Identifier, - options?: Omit, 'where'> + options?: Omit>, 'where'> ): Promise; /** @@ -1883,11 +1890,11 @@ export abstract class Model( this: ModelStatic, - options: NonNullFindOptions + options: NonNullFindOptions> ): Promise; public static findOne( this: ModelStatic, - options?: FindOptions + options?: FindOptions> ): Promise; /** @@ -1901,9 +1908,9 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'] | '*', + field: keyof Attributes | '*', aggregateFunction: string, - options?: AggregateOptions + options?: AggregateOptions> ): Promise; /** @@ -1912,7 +1919,7 @@ export abstract class Model( this: ModelStatic, - options: CountWithOptions + options: CountWithOptions> ): Promise; /** @@ -1923,7 +1930,7 @@ export abstract class Model( this: ModelStatic, - options?: Omit, 'group'> + options?: Omit>, 'group'> ): Promise; /** @@ -1971,11 +1978,11 @@ export abstract class Model( this: ModelStatic, - options?: Omit, 'group'> + options?: Omit>, 'group'> ): Promise<{ rows: M[]; count: number }>; public static findAndCountAll( this: ModelStatic, - options: SetRequired, 'group'> + options: SetRequired>, 'group'> ): Promise<{ rows: M[]; count: GroupedCountResultItem[] }>; /** @@ -1983,8 +1990,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options?: AggregateOptions + field: keyof Attributes, + options?: AggregateOptions> ): Promise; /** @@ -1992,8 +1999,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options?: AggregateOptions + field: keyof Attributes, + options?: AggregateOptions> ): Promise; /** @@ -2001,8 +2008,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options?: AggregateOptions + field: keyof Attributes, + options?: AggregateOptions> ): Promise; /** @@ -2010,7 +2017,7 @@ export abstract class Model( this: ModelStatic, - record?: M['_creationAttributes'], + record?: CreationAttributes, options?: BuildOptions ): M; @@ -2019,7 +2026,7 @@ export abstract class Model( this: ModelStatic, - records: ReadonlyArray, + records: ReadonlyArray>, options?: BuildOptions ): M[]; @@ -2028,10 +2035,10 @@ export abstract class Model = CreateOptions + O extends CreateOptions> = CreateOptions> >( this: ModelStatic, - values?: M['_creationAttributes'], + values?: CreationAttributes, options?: O ): Promise; @@ -2041,7 +2048,10 @@ export abstract class Model( this: ModelStatic, - options: FindOrBuildOptions + options: FindOrBuildOptions< + Attributes, + CreationAttributes + > ): Promise<[M, boolean]>; /** @@ -2057,7 +2067,7 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions + options: FindOrCreateOptions, CreationAttributes> ): Promise<[M, boolean]>; /** @@ -2066,7 +2076,7 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions + options: FindOrCreateOptions, CreationAttributes> ): Promise<[M, boolean]>; /** @@ -2090,8 +2100,8 @@ export abstract class Model( this: ModelStatic, - values: M['_creationAttributes'], - options?: UpsertOptions + values: CreationAttributes, + options?: UpsertOptions> ): Promise<[M, boolean | null]>; /** @@ -2107,8 +2117,8 @@ export abstract class Model( this: ModelStatic, - records: ReadonlyArray, - options?: BulkCreateOptions + records: ReadonlyArray>, + options?: BulkCreateOptions> ): Promise; /** @@ -2116,7 +2126,7 @@ export abstract class Model( this: ModelStatic, - options?: TruncateOptions + options?: TruncateOptions> ): Promise; /** @@ -2126,7 +2136,7 @@ export abstract class Model( this: ModelStatic, - options?: DestroyOptions + options?: DestroyOptions> ): Promise; /** @@ -2134,7 +2144,7 @@ export abstract class Model( this: ModelStatic, - options?: RestoreOptions + options?: RestoreOptions> ): Promise; /** @@ -2145,9 +2155,9 @@ export abstract class Model( this: ModelStatic, values: { - [key in keyof M['_attributes']]?: M['_attributes'][key] | Fn | Col | Literal; + [key in keyof Attributes]?: Attributes[key] | Fn | Col | Literal; }, - options: UpdateOptions + options: UpdateOptions> ): Promise<[number, M[]]>; /** @@ -2155,8 +2165,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options: IncrementDecrementOptionsWithBy + field: keyof Attributes, + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2164,8 +2174,8 @@ export abstract class Model( this: ModelStatic, - fields: ReadonlyArray, - options: IncrementDecrementOptionsWithBy + fields: ReadonlyArray>, + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2173,8 +2183,8 @@ export abstract class Model( this: ModelStatic, - fields: { [key in keyof M['_attributes']]?: number }, - options: IncrementDecrementOptions + fields: { [key in keyof Attributes]?: number }, + options: IncrementDecrementOptions> ): Promise; /** @@ -2182,8 +2192,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options: IncrementDecrementOptionsWithBy + field: keyof Attributes, + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2191,8 +2201,8 @@ export abstract class Model( this: ModelStatic, - fields: (keyof M['_attributes'])[], - options: IncrementDecrementOptionsWithBy + fields: (keyof Attributes)[], + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2200,8 +2210,8 @@ export abstract class Model( this: ModelStatic, - fields: { [key in keyof M['_attributes']]?: number }, - options: IncrementDecrementOptions + fields: { [key in keyof Attributes]?: number }, + options: IncrementDecrementOptions> ): Promise; /** @@ -2256,11 +2266,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; public static beforeCreate( this: ModelStatic, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; /** @@ -2272,11 +2282,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; public static afterCreate( this: ModelStatic, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; /** @@ -2320,11 +2330,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; public static beforeUpdate( this: ModelStatic, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; /** @@ -2336,11 +2346,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; public static afterUpdate( this: ModelStatic, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; /** @@ -2352,11 +2362,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; public static beforeSave( this: ModelStatic, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; /** @@ -2368,11 +2378,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; public static afterSave( this: ModelStatic, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; /** @@ -2384,11 +2394,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instances: M[], options: BulkCreateOptions) => HookReturn + fn: (instances: M[], options: BulkCreateOptions>) => HookReturn ): void; public static beforeBulkCreate( this: ModelStatic, - fn: (instances: M[], options: BulkCreateOptions) => HookReturn + fn: (instances: M[], options: BulkCreateOptions>) => HookReturn ): void; /** @@ -2400,11 +2410,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instances: readonly M[], options: BulkCreateOptions) => HookReturn + fn: (instances: readonly M[], options: BulkCreateOptions>) => HookReturn ): void; public static afterBulkCreate( this: ModelStatic, - fn: (instances: readonly M[], options: BulkCreateOptions) => HookReturn + fn: (instances: readonly M[], options: BulkCreateOptions>) => HookReturn ): void; /** @@ -2415,10 +2425,10 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: BulkCreateOptions) => HookReturn): void; + name: string, fn: (options: BulkCreateOptions>) => HookReturn): void; public static beforeBulkDestroy( this: ModelStatic, - fn: (options: BulkCreateOptions) => HookReturn + fn: (options: BulkCreateOptions>) => HookReturn ): void; /** @@ -2429,11 +2439,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: DestroyOptions) => HookReturn + name: string, fn: (options: DestroyOptions>) => HookReturn ): void; public static afterBulkDestroy( this: ModelStatic, - fn: (options: DestroyOptions) => HookReturn + fn: (options: DestroyOptions>) => HookReturn ): void; /** @@ -2444,11 +2454,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: UpdateOptions) => HookReturn + name: string, fn: (options: UpdateOptions>) => HookReturn ): void; public static beforeBulkUpdate( this: ModelStatic, - fn: (options: UpdateOptions) => HookReturn + fn: (options: UpdateOptions>) => HookReturn ): void; /** @@ -2459,11 +2469,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: UpdateOptions) => HookReturn + name: string, fn: (options: UpdateOptions>) => HookReturn ): void; public static afterBulkUpdate( this: ModelStatic, - fn: (options: UpdateOptions) => HookReturn + fn: (options: UpdateOptions>) => HookReturn ): void; /** @@ -2474,11 +2484,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: FindOptions) => HookReturn + name: string, fn: (options: FindOptions>) => HookReturn ): void; public static beforeFind( this: ModelStatic, - fn: (options: FindOptions) => HookReturn + fn: (options: FindOptions>) => HookReturn ): void; /** @@ -2489,11 +2499,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: CountOptions) => HookReturn + name: string, fn: (options: CountOptions>) => HookReturn ): void; public static beforeCount( this: ModelStatic, - fn: (options: CountOptions) => HookReturn + fn: (options: CountOptions>) => HookReturn ): void; /** @@ -2504,11 +2514,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: FindOptions) => HookReturn + name: string, fn: (options: FindOptions>) => HookReturn ): void; public static beforeFindAfterExpandIncludeAll( this: ModelStatic, - fn: (options: FindOptions) => HookReturn + fn: (options: FindOptions>) => HookReturn ): void; /** @@ -2519,11 +2529,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: FindOptions) => HookReturn + name: string, fn: (options: FindOptions>) => HookReturn ): void; public static beforeFindAfterOptions( this: ModelStatic, - fn: (options: FindOptions) => void + fn: (options: FindOptions>) => void ): HookReturn; /** @@ -2535,11 +2545,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions) => HookReturn + fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions>) => HookReturn ): void; public static afterFind( this: ModelStatic, - fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions) => HookReturn + fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions>) => HookReturn ): void; /** @@ -2720,7 +2730,7 @@ export abstract class Model, options?: BuildOptions); /** * Get an object representing the query for this instance, use with `options.where` @@ -2795,8 +2805,8 @@ export abstract class Model; - public previous(key: K): TCreationAttributes[K] | undefined; + public previous(): Partial; + public previous(key: K): TModelAttributes[K] | undefined; /** * Validates this instance, and if the validation passes, persists it to the database. @@ -2941,3 +2951,174 @@ export type ModelDefined = ModelStatic>; export type ModelStatic = NonConstructor & { new(): M }; export default Model; + +/** + * Dummy Symbol used as branding by {@link NonAttribute}. + * + * Do not export, Do not use. + */ +declare const NonAttributeBrand: unique symbol; +interface NonAttributeBrandedArray extends Array { + [NonAttributeBrand]: true +} + +/** + * This is a Branded Type. + * You can use it to tag fields from your class that are NOT attributes. + * They will be ignored by {@link InferAttributes} and {@link InferCreationAttributes} + */ +export type NonAttribute = T extends Array + // Arrays are a special case when branding. Both sides need to be an array, + // otherwise property access breaks. + ? T | NonAttributeBrandedArray + : T | { [NonAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. + +/** + * Option bag for {@link InferAttributes}. + * + * - omit: properties to not treat as Attributes. + */ +type InferAttributesOptions = { omit?: Excluded }; + +/** + * Utility type to extract Attributes of a given Model class. + * + * It returns all instance properties defined in the Model, except: + * - those inherited from Model (intermediate inheritance works), + * - the ones whose type is a function, + * - the ones manually excluded using the second parameter. + * - the ones branded using {@link NonAttribute} + * + * It cannot detect whether something is a getter or not, you should use the `Excluded` + * parameter to exclude getter & setters from the attribute list. + * + * @example + * // listed attributes will be 'id' & 'firstName'. + * class User extends Model> { + * id: number; + * firstName: string; + * } + * + * @example + * // listed attributes will be 'id' & 'firstName'. + * // we're excluding the `name` getter & `projects` attribute using the `omit` option. + * class User extends Model> { + * id: number; + * firstName: string; + * + * // this is a getter, not an attribute. It should not be listed in attributes. + * get name(): string { return this.firstName; } + * // this is an association, it should not be listed in attributes + * projects?: Project[]; + * } + * + * @example + * // listed attributes will be 'id' & 'firstName'. + * // we're excluding the `name` getter & `test` attribute using the `NonAttribute` branded type. + * class User extends Model> { + * id: number; + * firstName: string; + * + * // this is a getter, not an attribute. It should not be listed in attributes. + * get name(): NonAttribute { return this.firstName; } + * // this is an association, it should not be listed in attributes + * projects?: NonAttribute; + * } + */ +export type InferAttributes< + M extends Model, + Options extends InferAttributesOptions = { omit: never } +> = { + [Key in keyof M as + M[Key] extends AnyFunction ? never + : { [NonAttributeBrand]: true } extends M[Key] ? never + // array special case + : M[Key] extends NonAttributeBrandedArray ? never + : Key extends keyof Model ? never + // check 'omit' option is provided + : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) + : Key + ]: M[Key] +}; + +/** + * Dummy Symbol used as branding by {@link CreationOptional}. + * + * Do not export, Do not use. + */ +declare const CreationAttributeBrand: unique symbol; +interface CreationAttributeBrandedArray extends Array { + [CreationAttributeBrand]: true +} + +/** + * This is a Branded Type. + * You can use it to tag attributes that can be ommited during Model Creation. + * + * For use with {@link InferCreationAttributes}. + */ +export type CreationOptional = T extends Array + // Arrays are a special case when branding. Both sides need to be an array, + // otherwise property access breaks. + ? T | CreationAttributeBrandedArray + : T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. + +/** + * Utility type to extract Creation Attributes of a given Model class. + * + * Works like {@link InferAttributes}, but fields that are tagged using + * {@link CreationOptional} will be optional. + * + * @example + * class User extends Model, InferCreationAttributes> { + * // this attribute is optional in Model#create + * declare id: CreationOptional; + * + * // this attribute is mandatory in Model#create + * declare name: string; + * } + */ +export type InferCreationAttributes< + M extends Model, + Options extends InferAttributesOptions = { omit: never } +> = { + [Key in keyof M as + M[Key] extends AnyFunction ? never + : { [NonAttributeBrand]: true } extends M[Key] ? never + // array special case + : M[Key] extends NonAttributeBrandedArray ? never + : Key extends keyof Model ? never + // check 'omit' option is provided + : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) + : Key + // it is normal that the brand extends the type. + // We're checking if it's in the type. + ]: { [CreationAttributeBrand]: true } extends M[Key] ? (M[Key] | undefined) + // array special case + : CreationAttributeBrandedArray extends M[Key] ? (M[Key] | undefined) + : M[Key] +}; + +// in v7, we should be able to drop InferCreationAttributes and InferAttributes, +// resolving this confusion. +/** + * Returns the creation attributes of a given Model. + * + * This returns the Creation Attributes of a Model, it does not build them. + * If you need to build them, use {@link InferCreationAttributes}. + * + * @example + * function buildModel(modelClass: ModelStatic, attributes: CreationAttributes) {} + */ +export type CreationAttributes = MakeUndefinedOptional; + +/** + * Returns the creation attributes of a given Model. + * + * This returns the Attributes of a Model that have already been defined, it does not build them. + * If you need to build them, use {@link InferAttributes}. + * + * @example + * function getValue(modelClass: ModelStatic, attribute: keyof Attributes) {} + */ +export type Attributes = M['_attributes']; diff --git a/types/lib/query-interface.d.ts b/types/lib/query-interface.d.ts index 01c8eef3ca7e..a2bf04b6bcd4 100644 --- a/types/lib/query-interface.d.ts +++ b/types/lib/query-interface.d.ts @@ -8,7 +8,11 @@ import { WhereOptions, Filterable, Poolable, - ModelCtor, ModelStatic, ModelType + ModelCtor, + ModelStatic, + ModelType, + CreationAttributes, + Attributes, } from './model'; import { QueryTypes, Transaction } from '..'; import { Sequelize, RetryOptions } from './sequelize'; @@ -336,7 +340,7 @@ export class QueryInterface { */ public createTable( tableName: TableName, - attributes: ModelAttributes, + attributes: ModelAttributes>, options?: QueryInterfaceCreateTableOptions ): Promise; @@ -507,7 +511,7 @@ export class QueryInterface { instance: M, tableName: TableName, values: object, - identifier: WhereOptions, + identifier: WhereOptions>, options?: QueryOptions ): Promise; @@ -554,7 +558,7 @@ export class QueryInterface { instance: Model, tableName: TableName, values: object, - identifier: WhereOptions, + identifier: WhereOptions>, options?: QueryOptions ): Promise; diff --git a/types/lib/sequelize.d.ts b/types/lib/sequelize.d.ts index 67b0fcfd264d..dec11107fabb 100644 --- a/types/lib/sequelize.d.ts +++ b/types/lib/sequelize.d.ts @@ -20,6 +20,8 @@ import { ModelCtor, Hookable, ModelType, + CreationAttributes, + Attributes, } from './model'; import { ModelManager } from './model-manager'; import { QueryInterface, QueryOptions, QueryOptionsWithModel, QueryOptionsWithType, ColumnsDescription } from './query-interface'; @@ -751,10 +753,10 @@ export class Sequelize extends Hooks { */ public static beforeDefine( name: string, - fn: (attributes: ModelAttributes, options: ModelOptions) => void + fn: (attributes: ModelAttributes>, options: ModelOptions) => void ): void; public static beforeDefine( - fn: (attributes: ModelAttributes, options: ModelOptions) => void + fn: (attributes: ModelAttributes>, options: ModelOptions) => void ): void; /** @@ -1191,7 +1193,7 @@ export class Sequelize extends Hooks { * @param options These options are merged with the default define options provided to the Sequelize * constructor */ - public define( + public define>( modelName: string, attributes: ModelAttributes, options?: ModelOptions diff --git a/types/lib/utils.d.ts b/types/lib/utils.d.ts index 02552a46c1f8..de1ee23dca2e 100644 --- a/types/lib/utils.d.ts +++ b/types/lib/utils.d.ts @@ -1,5 +1,6 @@ import { DataType } from './data-types'; -import { Model, ModelCtor, ModelType, WhereOptions } from './model'; +import { Model, ModelCtor, ModelType, WhereOptions, Attributes } from './model'; +import { Optional } from '..'; export type Primitive = 'string' | 'number' | 'boolean'; @@ -32,13 +33,13 @@ export interface OptionsForMapping { } /** Expand and normalize finder options */ -export function mapFinderOptions>( +export function mapFinderOptions>>( options: T, model: ModelCtor ): T; /* Used to map field names in attributes and where conditions */ -export function mapOptionFieldNames>( +export function mapOptionFieldNames>>( options: T, model: ModelCtor ): T; @@ -122,3 +123,44 @@ export class Where extends SequelizeMethod { constructor(attr: object, comparator: string, logic: string | object); constructor(attr: object, logic: string | object); } + +export type AnyFunction = (...args: any[]) => any; + +/** + * Returns all shallow properties that accept `undefined`. + * Does not include Optional properties, only `undefined`. + * + * @example + * type UndefinedProps = UndefinedPropertiesOf<{ + * id: number | undefined, + * createdAt: string | undefined, + * firstName: string, + * lastName?: string, // optional properties are not included. + * }>; + * + * // is equal to + * + * type UndefinedProps = 'id' | 'createdAt'; + */ +export type UndefinedPropertiesOf = { + [P in keyof T]-?: undefined extends T[P] ? P : never +}[keyof T]; + +/** + * Makes all shallow properties of an object `optional` if they accept `undefined` as a value. + * + * @example + * type MyOptionalType = MakeUndefinedOptional<{ + * id: number | undefined, + * name: string, + * }>; + * + * // is equal to + * + * type MyOptionalType = { + * // this property is optional. + * id?: number | undefined, + * name: string, + * }; + */ +export type MakeUndefinedOptional = Optional>; diff --git a/types/test/attributes.ts b/types/test/attributes.ts index 2c754923debd..c01ab6274af8 100644 --- a/types/test/attributes.ts +++ b/types/test/attributes.ts @@ -8,14 +8,12 @@ interface UserAttributes extends UserCreationAttributes { id: number; } -class User - extends Model - implements UserAttributes { - public id!: number; - public name!: string; - - public readonly projects?: Project[]; - public readonly address?: Address; +class User extends Model implements UserAttributes { + declare id: number; + declare name: string; + + declare readonly projects?: Project[]; + declare readonly address?: Address; } interface ProjectCreationAttributes { @@ -30,14 +28,14 @@ interface ProjectAttributes extends ProjectCreationAttributes { class Project extends Model implements ProjectAttributes { - public id!: number; - public ownerId!: number; - public name!: string; + declare id: number; + declare ownerId: number; + declare name: string; } class Address extends Model { - public userId!: number; - public address!: string; + declare userId: number; + declare address: string; } // both models should be accepted in include diff --git a/types/test/create.ts b/types/test/create.ts index a370249b47f6..ab58c11fd416 100644 --- a/types/test/create.ts +++ b/types/test/create.ts @@ -1,7 +1,7 @@ import { expectTypeOf } from 'expect-type' import { User } from './models/User'; -async () => { +(async () => { const user = await User.create({ id: 123, firstName: '', @@ -71,4 +71,4 @@ async () => { // @ts-expect-error unknown attribute unknown: '', }); -}; +})(); diff --git a/types/test/infer-attributes.ts b/types/test/infer-attributes.ts new file mode 100644 index 000000000000..1892acb5b6aa --- /dev/null +++ b/types/test/infer-attributes.ts @@ -0,0 +1,123 @@ +import { expectTypeOf } from 'expect-type'; +import { InferAttributes, InferCreationAttributes, CreationOptional, Model, NonAttribute, Attributes, CreationAttributes } from 'sequelize'; + +class User extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: CreationOptional; + declare name: string; + declare anArray: CreationOptional; + + // omitted using `omit` option + declare groups: Group[]; + // omitted using `NonAttribute` + declare projects: NonAttribute; + + instanceMethod() {} + static staticMethod() {} +} + +type UserAttributes = Attributes; +type UserCreationAttributes = CreationAttributes; + +expectTypeOf().not.toBeAny(); + +{ + class Test extends Model> { + declare id: NonAttribute; + } + + const win: Attributes = {}; +} + +{ + const win: UserAttributes = { + id: 1, + name: '', + anArray: [''], + }; + + const fail1: UserAttributes = { + id: 1, + name: '', + // @ts-expect-error - 'extra' should not be present + extra: '' + }; + + // @ts-expect-error - 'name' should be present + const fail2: UserAttributes = { + id: 1, + }; +} + +{ + const win: UserCreationAttributes = { + id: undefined, + name: '', + anArray: undefined, + }; + + const fail1: UserCreationAttributes = { + id: 1, + name: '', + // @ts-expect-error 'extra' does not exist + extra: '' + }; + + const fail2: UserCreationAttributes = { + id: 1, + // @ts-expect-error name cannot be undefined + name: undefined, + anArray: undefined, + }; +} + +type GroupAttributes = InferAttributes; + +class Group extends Model { + declare id: number; +} + +{ + // @ts-expect-error - id should not be missing + const fail1: GroupAttributes = {}; + + // @ts-expect-error - id should not be missing + const fail2: InferAttributes = {}; + + // @ts-expect-error - id should not be missing + const fail3: InferAttributes = {}; +} + +class Project extends Model> { + declare id: number; +} + +// brands: + +{ + // ensure branding does not break arrays. + const brandedArray: NonAttribute = ['']; + const anArray: string[] = brandedArray; + const item: string = brandedArray[0]; +} + +{ + // ensure branding does not break objects + const brandedObject: NonAttribute> = {}; + const anObject: Record = brandedObject; + const item: string = brandedObject.key; +} + +{ + // ensure branding does not break primitives + const brandedString: NonAttribute = ''; + const aString: string = brandedString; +} + +{ + // ensure branding does not break instances + const brandedUser: NonAttribute = new User(); + const aUser: User = brandedUser; +} diff --git a/types/test/models/User.ts b/types/test/models/User.ts index b1a3d23abf90..e2029fb062f7 100644 --- a/types/test/models/User.ts +++ b/types/test/models/User.ts @@ -1,93 +1,92 @@ import { + InferAttributes, BelongsTo, BelongsToCreateAssociationMixin, BelongsToGetAssociationMixin, BelongsToSetAssociationMixin, + InferCreationAttributes, + CreationOptional, DataTypes, FindOptions, Model, - ModelCtor, - Op, - Optional + ModelStatic, + Op } from 'sequelize'; import { sequelize } from '../connection'; -export interface UserAttributes { - id: number; - username: string; - firstName: string; - lastName: string; - groupId: number; -} - -/** - * In this case, we make most fields optional. In real cases, - * only fields that have default/autoincrement values should be made optional. - */ -export interface UserCreationAttributes extends Optional {} +type NonUserAttributes = 'group'; -export class User extends Model implements UserAttributes { +export class User extends Model< + InferAttributes, + InferCreationAttributes +> { public static associations: { group: BelongsTo; }; - public id!: number; - public username!: string; - public firstName!: string; - public lastName!: string; - public groupId!: number; - public createdAt!: Date; - public updatedAt!: Date; + declare id: CreationOptional; + declare createdAt: CreationOptional; + declare updatedAt: CreationOptional; + + declare username: CreationOptional; + declare firstName: string; + declare lastName: CreationOptional; + declare groupId: CreationOptional; // mixins for association (optional) - public group?: UserGroup; - public getGroup!: BelongsToGetAssociationMixin; - public setGroup!: BelongsToSetAssociationMixin; - public createGroup!: BelongsToCreateAssociationMixin; + declare group?: UserGroup; + declare getGroup: BelongsToGetAssociationMixin; + declare setGroup: BelongsToSetAssociationMixin; + declare createGroup: BelongsToCreateAssociationMixin; } User.init( { id: { type: DataTypes.NUMBER, - primaryKey: true, + primaryKey: true + }, + firstName: { + type: DataTypes.STRING, + allowNull: false, }, - firstName: DataTypes.STRING, lastName: DataTypes.STRING, username: DataTypes.STRING, groupId: DataTypes.NUMBER, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { version: true, getterMethods: { a() { return 1; - }, + } }, setterMethods: { b(val: string) { this.username = val; - }, + } }, scopes: { custom(a: number) { return { where: { - firstName: a, - }, + firstName: a + } }; }, custom2() { - return {} + return {}; } }, indexes: [{ fields: ['firstName'], using: 'BTREE', name: 'firstNameIdx', - concurrently: true, + concurrently: true }], - sequelize, + sequelize } ); @@ -96,9 +95,9 @@ User.afterSync(() => { fields: ['lastName'], using: 'BTREE', name: 'lastNameIdx', - concurrently: true, - }) -}) + concurrently: true + }); +}); // Hooks User.afterFind((users, options) => { @@ -106,7 +105,7 @@ User.afterFind((users, options) => { }); // TODO: VSCode shows the typing being correctly narrowed but doesn't do it correctly -User.addHook('beforeFind', 'test', (options: FindOptions) => { +User.addHook('beforeFind', 'test', (options: FindOptions>) => { return undefined; }); @@ -119,22 +118,22 @@ User.addHook('afterDestroy', async (instance, options) => { User.addScope('withoutFirstName', { where: { firstName: { - [Op.is]: null, - }, - }, + [Op.is]: null + } + } }); User.addScope( 'withFirstName', (firstName: string) => ({ - where: { firstName }, - }), + where: { firstName } + }) ); // associate // it is important to import _after_ the model above is already exported so the circular reference works. import { UserGroup } from './UserGroup'; -import { UserPost } from "./UserPost"; +import { UserPost } from './UserPost'; // associate with a class-based model export const Group = User.belongsTo(UserGroup, { as: 'group', foreignKey: 'groupId' }); @@ -143,21 +142,21 @@ User.hasMany(UserPost, { as: 'posts', foreignKey: 'userId' }); UserPost.belongsTo(User, { foreignKey: 'userId', targetKey: 'id', - as: 'user', + as: 'user' }); // associations refer to their Model -const userType: ModelCtor = User.associations.group.source; -const groupType: ModelCtor = User.associations.group.target; +const userType: ModelStatic = User.associations.group.source; +const groupType: ModelStatic = User.associations.group.target; // should associate correctly with both sequelize.define and class-based models -User.findOne({ include: [{ model: UserGroup }]}); -User.findOne({ include: [{ model: UserPost }]}); +User.findOne({ include: [{ model: UserGroup }] }); +User.findOne({ include: [{ model: UserPost }] }); User.scope([ 'custom2', - { method: [ 'custom', 32 ] } -]) + { method: ['custom', 32] } +]); const instance = new User({ username: 'foo', firstName: 'bar', lastName: 'baz' }); -instance.isSoftDeleted() +instance.isSoftDeleted(); diff --git a/types/test/models/UserGroup.ts b/types/test/models/UserGroup.ts index 31c385ab2044..e50d4ac6eec9 100644 --- a/types/test/models/UserGroup.ts +++ b/types/test/models/UserGroup.ts @@ -1,16 +1,19 @@ import { - DataTypes, - HasMany, - HasManyAddAssociationMixin, - HasManyAddAssociationsMixin, - HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, - HasManyGetAssociationsMixin, - HasManyHasAssociationMixin, - HasManyRemoveAssociationMixin, - HasManyRemoveAssociationsMixin, - HasManySetAssociationsMixin, - Model + InferAttributes, + InferCreationAttributes, + CreationOptional, + DataTypes, + HasMany, + HasManyAddAssociationMixin, + HasManyAddAssociationsMixin, + HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, + HasManyGetAssociationsMixin, + HasManyHasAssociationMixin, + HasManyRemoveAssociationMixin, + HasManyRemoveAssociationsMixin, + HasManySetAssociationsMixin, + Model } from 'sequelize'; import { sequelize } from '../connection'; // associate @@ -19,29 +22,40 @@ import { User } from './User'; // This class doesn't extend the generic Model, but should still // function just fine, with a bit less safe type-checking -export class UserGroup extends Model { - public static associations: { - users: HasMany - }; +export class UserGroup extends Model< + InferAttributes, + InferCreationAttributes +> { + public static associations: { + users: HasMany + }; - public id!: number; - public name!: string; + declare id: CreationOptional; + declare name: string; - // mixins for association (optional) - public users!: User[]; - public getUsers!: HasManyGetAssociationsMixin; - public setUsers!: HasManySetAssociationsMixin; - public addUser!: HasManyAddAssociationMixin; - public addUsers!: HasManyAddAssociationsMixin; - public createUser!: HasManyCreateAssociationMixin; - public countUsers!: HasManyCountAssociationsMixin; - public hasUser!: HasManyHasAssociationMixin; - public removeUser!: HasManyRemoveAssociationMixin; - public removeUsers!: HasManyRemoveAssociationsMixin; + // mixins for association (optional) + declare users?: User[]; + declare getUsers: HasManyGetAssociationsMixin; + declare setUsers: HasManySetAssociationsMixin; + declare addUser: HasManyAddAssociationMixin; + declare addUsers: HasManyAddAssociationsMixin; + declare createUser: HasManyCreateAssociationMixin; + declare countUsers: HasManyCountAssociationsMixin; + declare hasUser: HasManyHasAssociationMixin; + declare removeUser: HasManyRemoveAssociationMixin; + declare removeUsers: HasManyRemoveAssociationsMixin; } // attach all the metadata to the model // instead of this, you could also use decorators -UserGroup.init({ name: DataTypes.STRING }, { sequelize }); +UserGroup.init({ + name: DataTypes.STRING, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + } +}, { sequelize }); export const Users = UserGroup.hasMany(User, { as: 'users', foreignKey: 'groupId' }); diff --git a/types/test/typescriptDocs/Define.ts b/types/test/typescriptDocs/Define.ts index 9e222311053d..8ea23694d01c 100644 --- a/types/test/typescriptDocs/Define.ts +++ b/types/test/typescriptDocs/Define.ts @@ -1,6 +1,8 @@ /** * Keep this file in sync with the code in the "Usage of `sequelize.define`" - * section in typescript.md + * section in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Sequelize, Model, DataTypes, Optional } from 'sequelize'; diff --git a/types/test/typescriptDocs/DefineNoAttributes.ts b/types/test/typescriptDocs/DefineNoAttributes.ts index ac2d70069a13..8273cad18ff1 100644 --- a/types/test/typescriptDocs/DefineNoAttributes.ts +++ b/types/test/typescriptDocs/DefineNoAttributes.ts @@ -1,6 +1,8 @@ /** * Keep this file in sync with the code in the "Usage of `sequelize.define`" - * that doesn't have attribute types in typescript.md + * that doesn't have attribute types in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Sequelize, Model, DataTypes } from 'sequelize'; diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index d0720fe2628f..ea7ebe6cd6e3 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -1,120 +1,114 @@ /** - * Keep this file in sync with the code in the "Usage" section in typescript.md + * Keep this file in sync with the code in the "Usage" section + * in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize -} 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; -} - -// Some attributes are optional in `User.build` and `User.create` calls -interface UserCreationAttributes extends Optional {} - -class User extends Model - implements UserAttributes { - declare id: number; // Note that the `null assertion` `!` is required in strict mode. + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, + HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, + Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute +} from 'sequelize'; + +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); + +// 'projects' is excluded as it's not an attribute, it's an association. +class User extends Model, InferCreationAttributes> { + // id can be undefined during creation when using `autoIncrement` + declare id: CreationOptional; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! - declare readonly createdAt: Date; - declare readonly updatedAt: Date; + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; // Since TS cannot determine model association at compile time // we have to declare them here purely virtually // these will not exist until `Model.init` was called. declare getProjects: HasManyGetAssociationsMixin; // Note the null assertions! declare addProject: HasManyAddAssociationMixin; + declare addProjects: HasManyAddAssociationsMixin; + declare setProjects: HasManySetAssociationsMixin; + declare removeProject: HasManyRemoveAssociationMixin; + declare removeProjects: HasManyRemoveAssociationsMixin; declare hasProject: HasManyHasAssociationMixin; + declare hasProjects: HasManyHasAssociationsMixin; declare countProjects: HasManyCountAssociationsMixin; declare createProject: HasManyCreateAssociationMixin; // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + declare projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + + // getters that are not attributes should be tagged using NonAttribute + // to remove them from the model's Attribute Typings. + get fullName(): NonAttribute { + return this.name; + } declare static associations: { projects: Association; }; } -interface ProjectAttributes { - id: number; - ownerId: number; - name: string; - description?: string; -} - -interface ProjectCreationAttributes extends Optional {} - -class Project extends Model - implements ProjectAttributes { - declare id: number; +class Project extends Model< + InferAttributes, + InferCreationAttributes +> { + // id can be undefined during creation when using `autoIncrement` + declare id: CreationOptional; declare ownerId: number; declare name: string; - declare readonly createdAt: Date; - declare readonly updatedAt: Date; -} + // `owner` is an eagerly-loaded association. + // We tag it as `NonAttribute` + declare owner?: NonAttribute; -interface AddressAttributes { - userId: number; - address: string; + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; } -// You can write `extends Model` instead, -// but that will do the exact same thing as below -class Address extends Model implements AddressAttributes { +class Address extends Model< + InferAttributes
, + InferCreationAttributes
+> { declare userId: number; declare address: string; - declare readonly createdAt: Date; - declare readonly updatedAt: Date; + // createdAt can be undefined during creation + declare createdAt: CreationOptional; + // updatedAt can be undefined during creation + declare updatedAt: CreationOptional; } -// You can also define modules in a functional way -interface NoteAttributes { - id: number; - title: string; - content: string; -} - -// You can also set multiple attributes optional at once -interface NoteCreationAttributes - extends Optional {} - Project.init( { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, ownerId: { type: DataTypes.INTEGER.UNSIGNED, - allowNull: false, + allowNull: false }, name: { type: new DataTypes.STRING(128), - allowNull: false, - }, - description: { - type: new DataTypes.STRING(128), - allowNull: true, + allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { sequelize, - tableName: "projects", + tableName: 'projects' } ); @@ -123,89 +117,103 @@ User.init( id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, name: { type: new DataTypes.STRING(128), - allowNull: false, + allowNull: false }, preferredName: { type: new DataTypes.STRING(128), - allowNull: true, + allowNull: true }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { - tableName: "users", - sequelize, // passing the `sequelize` instance is required + tableName: 'users', + sequelize // passing the `sequelize` instance is required } ); Address.init( { userId: { - type: DataTypes.INTEGER.UNSIGNED, + type: DataTypes.INTEGER.UNSIGNED }, address: { type: new DataTypes.STRING(128), - allowNull: false, + allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { - tableName: "address", - sequelize, // passing the `sequelize` instance is required + tableName: 'address', + sequelize // passing the `sequelize` instance is required } ); +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +type NoteCreationAttributes = Optional; + // And with a functional approach defining a module looks like this const Note: ModelDefined< NoteAttributes, NoteCreationAttributes > = sequelize.define( - "Note", + 'Note', { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, title: { type: new DataTypes.STRING(64), - defaultValue: "Unnamed Note", + defaultValue: 'Unnamed Note' }, content: { type: new DataTypes.STRING(4096), - allowNull: false, - }, + allowNull: false + } }, { - tableName: "notes", + tableName: 'notes' } ); // Here we associate which actually populates out pre-declared `association` static and other methods. User.hasMany(Project, { - sourceKey: "id", - foreignKey: "ownerId", - as: "projects", // this determines the name in `associations`! + sourceKey: 'id', + foreignKey: 'ownerId', + as: 'projects' // this determines the name in `associations`! }); -Address.belongsTo(User, { targetKey: "id" }); -User.hasOne(Address, { sourceKey: "id" }); +Address.belongsTo(User, { targetKey: 'id' }); +User.hasOne(Address, { sourceKey: 'id' }); async function doStuffWithUser() { const newUser = await User.create({ - name: "Johnny", - preferredName: "John", + name: 'Johnny', + preferredName: 'John', }); console.log(newUser.id, newUser.name, newUser.preferredName); const project = await newUser.createProject({ - name: "first!", + name: 'first!' }); const ourUser = await User.findByPk(1, { include: [User.associations.projects], - rejectOnEmpty: true, // Specifying true here removes `null` from the return type! + rejectOnEmpty: true // Specifying true here removes `null` from the return type! }); // Note the `!` null assertion since TS can't know if we included diff --git a/types/test/typescriptDocs/ModelInitNoAttributes.ts b/types/test/typescriptDocs/ModelInitNoAttributes.ts index b53ea86fc12a..ff8a5c22a22d 100644 --- a/types/test/typescriptDocs/ModelInitNoAttributes.ts +++ b/types/test/typescriptDocs/ModelInitNoAttributes.ts @@ -1,6 +1,8 @@ /** * Keep this file in sync with the code in the "Usage without strict types for - * attributes" section in typescript.md + * attributes" section in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Sequelize, Model, DataTypes } from 'sequelize';