From cf35da9ff232e68fc4cedc0e4eb624ef44814dca Mon Sep 17 00:00:00 2001 From: ephys Date: Thu, 10 Feb 2022 22:37:58 +0100 Subject: [PATCH 1/5] feat: remove deprecated model types --- src/associations/base.d.ts | 6 +++--- src/associations/belongs-to-many.d.ts | 9 ++++----- src/associations/belongs-to.d.ts | 5 ++--- src/associations/has-many.d.ts | 4 ++-- src/associations/has-one.d.ts | 4 ++-- src/dialects/abstract/query-interface.d.ts | 9 ++++----- src/dialects/abstract/query.d.ts | 8 ++++---- src/hooks.d.ts | 4 ++-- src/model-manager.d.ts | 6 +++--- src/model.d.ts | 20 ++++++-------------- src/sequelize.d.ts | 17 ++++++++--------- src/utils.d.ts | 10 +++++----- test/types/model.ts | 7 +++---- test/types/sequelize.ts | 4 ++-- test/types/type-helpers/deep-writable.ts | 4 ---- test/types/typescriptDocs/ModelInit.ts | 6 +++--- 16 files changed, 53 insertions(+), 70 deletions(-) diff --git a/src/associations/base.d.ts b/src/associations/base.d.ts index 186005992fed..cc54aa1da621 100644 --- a/src/associations/base.d.ts +++ b/src/associations/base.d.ts @@ -1,9 +1,9 @@ -import { ColumnOptions, Model, ModelCtor, Hookable } from '../model'; +import { ColumnOptions, Model, ModelStatic, Hookable } from '../model'; export abstract class Association { public associationType: string; - public source: ModelCtor; - public target: ModelCtor; + public source: ModelStatic; + public target: ModelStatic; public isSelfAssociation: boolean; public isSingleAssociation: boolean; public isMultiAssociation: boolean; diff --git a/src/associations/belongs-to-many.d.ts b/src/associations/belongs-to-many.d.ts index af3e3075cc86..29e2320b2c13 100644 --- a/src/associations/belongs-to-many.d.ts +++ b/src/associations/belongs-to-many.d.ts @@ -8,8 +8,7 @@ import { InstanceDestroyOptions, InstanceUpdateOptions, Model, - ModelCtor, - ModelType, + ModelStatic, Transactionable, } from '../model'; import { Association, AssociationScope, ForeignKeyOptions, ManyToManyOptions, MultiAssociationAccessors } from './base'; @@ -22,7 +21,7 @@ export interface ThroughOptions { * The model used to join both sides of the N:M association. * Can be a string if you want the model to be generated by sequelize. */ - model: ModelType | string; + model: ModelStatic | string; /** * If true the generated join table will be paranoid @@ -60,7 +59,7 @@ export interface BelongsToManyOptions extends ManyToManyOptions { * The name of the table that is used to join source and target in n:m associations. Can also be a * sequelize model if you want to define the junction table yourself and add extra attributes to it. */ - through: ModelType | string | ThroughOptions; + through: ModelStatic | string | ThroughOptions; /** * The name of the foreign key in the join table (representing the target model) or an object representing @@ -98,7 +97,7 @@ export class BelongsToMany ext public sourceKey: string; public targetKey: string; public accessors: MultiAssociationAccessors; - constructor(source: ModelCtor, target: ModelCtor, options: BelongsToManyOptions); + constructor(source: ModelStatic, target: ModelStatic, options: BelongsToManyOptions); } /** diff --git a/src/associations/belongs-to.d.ts b/src/associations/belongs-to.d.ts index 19d50859290e..58171edb37b2 100644 --- a/src/associations/belongs-to.d.ts +++ b/src/associations/belongs-to.d.ts @@ -1,8 +1,7 @@ import { DataType } from '../data-types'; -import { CreateOptions, CreationAttributes, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; +import { CreateOptions, CreationAttributes, FindOptions, Model, ModelStatic, SaveOptions } from '../model'; import { Association, AssociationOptions, SingleAssociationAccessors } from './base'; -// type ModelCtor = InstanceType; /** * Options provided when associating models with belongsTo relationship * @@ -23,7 +22,7 @@ export interface BelongsToOptions extends AssociationOptions { export class BelongsTo extends Association { public accessors: SingleAssociationAccessors; - constructor(source: ModelCtor, target: ModelCtor, options: BelongsToOptions); + constructor(source: ModelStatic, target: ModelStatic, options: BelongsToOptions); } /** diff --git a/src/associations/has-many.d.ts b/src/associations/has-many.d.ts index ee83127b845a..77ac58b0787d 100644 --- a/src/associations/has-many.d.ts +++ b/src/associations/has-many.d.ts @@ -6,7 +6,7 @@ import { FindOptions, InstanceUpdateOptions, Model, - ModelCtor, + ModelStatic, Transactionable, } from '../model'; import { Association, ManyToManyOptions, MultiAssociationAccessors } from './base'; @@ -30,7 +30,7 @@ export interface HasManyOptions extends ManyToManyOptions { export class HasMany extends Association { public accessors: MultiAssociationAccessors; - constructor(source: ModelCtor, target: ModelCtor, options: HasManyOptions); + constructor(source: ModelStatic, target: ModelStatic, options: HasManyOptions); } /** diff --git a/src/associations/has-one.d.ts b/src/associations/has-one.d.ts index 692b9e1efcbf..86a897f6ce96 100644 --- a/src/associations/has-one.d.ts +++ b/src/associations/has-one.d.ts @@ -1,5 +1,5 @@ import { DataType } from '../data-types'; -import { CreateOptions, CreationAttributes, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; +import { CreateOptions, CreationAttributes, FindOptions, Model, ModelStatic, SaveOptions } from '../model'; import { Association, AssociationOptions, SingleAssociationAccessors } from './base'; /** @@ -21,7 +21,7 @@ export interface HasOneOptions extends AssociationOptions { export class HasOne extends Association { public accessors: SingleAssociationAccessors; - constructor(source: ModelCtor, target: ModelCtor, options: HasOneOptions); + constructor(source: ModelStatic, target: ModelStatic, options: HasOneOptions); } /** diff --git a/src/dialects/abstract/query-interface.d.ts b/src/dialects/abstract/query-interface.d.ts index bc210e859f1e..1b6751adb1ed 100644 --- a/src/dialects/abstract/query-interface.d.ts +++ b/src/dialects/abstract/query-interface.d.ts @@ -9,7 +9,6 @@ import { Filterable, Poolable, ModelStatic, - ModelType, CreationAttributes, Attributes, } from '../../model'; @@ -490,7 +489,7 @@ export class QueryInterface { insertValues: object, updateValues: object, where: object, - model: ModelType, + model: ModelStatic, options?: QueryOptions ): Promise; @@ -543,13 +542,13 @@ export class QueryInterface { tableName: TableName, identifier: WhereOptions, options?: QueryOptions, - model?: ModelType + model?: ModelStatic ): Promise; /** * Returns selected rows */ - public select(model: ModelType | null, tableName: TableName, options?: QueryOptionsWithWhere): Promise; + public select(model: ModelStatic | null, tableName: TableName, options?: QueryOptionsWithWhere): Promise; /** * Increments a row value @@ -569,7 +568,7 @@ export class QueryInterface { tableName: TableName, options: QueryOptionsWithWhere, attributeSelector: string | string[], - model?: ModelType + model?: ModelStatic ): Promise; /** diff --git a/src/dialects/abstract/query.d.ts b/src/dialects/abstract/query.d.ts index 582b4de77bde..b75f291dc9ce 100644 --- a/src/dialects/abstract/query.d.ts +++ b/src/dialects/abstract/query.d.ts @@ -1,6 +1,6 @@ import { Connection } from './connection-manager'; import { QueryTypes } from '../../query-types'; -import { Model, ModelType, IncludeOptions } from '../../model'; +import { Model, ModelStatic, IncludeOptions } from '../../model'; import { Sequelize } from '../../sequelize'; type BindOrReplacements = { [key: string]: unknown } | unknown[]; @@ -13,7 +13,7 @@ export interface AbstractQueryGroupJoinDataOptions { export interface AbstractQueryOptions { instance?: Model; - model?: ModelType; + model?: ModelStatic; type?: QueryTypes; fieldMap?: boolean; @@ -79,10 +79,10 @@ export class AbstractQuery { /** * Model type definition. * - * @type {ModelType} + * @type {ModelStatic} * @memberof AbstractQuery */ - public model: ModelType; + public model: ModelStatic; /** * Returns the current sequelize instance. diff --git a/src/hooks.d.ts b/src/hooks.d.ts index bd3bc9355cde..34f6e8850cd7 100644 --- a/src/hooks.d.ts +++ b/src/hooks.d.ts @@ -9,7 +9,7 @@ import Model, { InstanceUpdateOptions, ModelAttributes, ModelOptions, RestoreOptions, UpdateOptions, UpsertOptions, - Attributes, CreationAttributes, ModelType + Attributes, CreationAttributes, ModelStatic } from './model'; import { AbstractQuery } from './dialects/abstract/query'; import { QueryOptions } from './dialects/abstract/query-interface'; @@ -71,7 +71,7 @@ export interface SequelizeHooks< TCreationAttributes = TAttributes > extends ModelHooks { beforeDefine(attributes: ModelAttributes, options: ModelOptions): void; - afterDefine(model: ModelType): void; + afterDefine(model: ModelStatic): void; beforeInit(config: Config, options: Options): void; afterInit(sequelize: Sequelize): void; beforeConnect(config: DeepWriteable): HookReturn; diff --git a/src/model-manager.d.ts b/src/model-manager.d.ts index 41e72dae6636..b66ee18cb68d 100644 --- a/src/model-manager.d.ts +++ b/src/model-manager.d.ts @@ -1,4 +1,4 @@ -import { Model, ModelType } from './model'; +import { Model, ModelStatic } from './model'; import { Sequelize } from './sequelize'; export class ModelManager { @@ -7,8 +7,8 @@ export class ModelManager { public all: typeof Model[]; constructor(sequelize: Sequelize); - public addModel(model: T): T; - public removeModel(model: ModelType): void; + public addModel>(model: T): T; + public removeModel(model: ModelStatic): void; public getModel(against: unknown, options?: { attribute?: string }): typeof Model; } diff --git a/src/model.d.ts b/src/model.d.ts index ab2addcef75d..8b5117a19ba3 100644 --- a/src/model.d.ts +++ b/src/model.d.ts @@ -407,7 +407,7 @@ export interface IncludeThroughOptions extends Filterable, Projectable { /** * Options for eager-loading associated models, also allowing for all associations to be loaded at once */ -export type Includeable = ModelType | Association | IncludeOptions | { all: true, nested?: true } | string; +export type Includeable = ModelStatic | Association | IncludeOptions | { all: true, nested?: true } | string; /** * Complex include options @@ -420,7 +420,7 @@ export interface IncludeOptions extends Filterable, Projectable, Paranoid { /** * The model you want to eagerly load */ - model?: ModelType; + model?: ModelStatic; /** * The alias of the relation, in case the model you want to eagerly load is aliassed. For `hasOne` / @@ -1313,7 +1313,7 @@ export interface ModelAttributeColumnReferencesOptions { /** * If this column references another table, provide it here as a Model, or a string */ - model?: TableName | ModelType; + model?: TableName | ModelStatic; /** * The column of the foreign table that this column references @@ -1759,7 +1759,7 @@ export abstract class Model, schema: string, options?: SchemaOptions - ): ModelCtor; + ): ModelStatic; /** * Get the tablename of the model, taking schema into account. The method will return The name as a string @@ -1828,7 +1828,7 @@ export abstract class Model( this: ModelStatic, options?: string | ScopeOptions | readonly (string | ScopeOptions)[] | WhereAttributeHash - ): ModelCtor; + ): ModelStatic; /** * Add a new scope to the model @@ -2272,7 +2272,7 @@ export abstract class Model(this: M): M; + public static unscoped>(this: M): M; /** * A hook that is run before validation @@ -2990,17 +2990,9 @@ export abstract class Model = new () => Model; - type NonConstructorKeys = ({[P in keyof T]: T[P] extends new () => any ? never : P })[keyof T]; type NonConstructor = Pick>; -/** @deprecated use ModelStatic */ -export type ModelCtor = ModelStatic; - -export type ModelDefined = ModelStatic>; - // remove the existing constructor that tries to return `Model<{},{}>` which would be incompatible with models that have typing defined & replace with proper constructor. export type ModelStatic = NonConstructor & { new(): M }; diff --git a/src/sequelize.d.ts b/src/sequelize.d.ts index 02824cf1f4e7..6c5571ada1be 100644 --- a/src/sequelize.d.ts +++ b/src/sequelize.d.ts @@ -17,9 +17,8 @@ import { UpdateOptions, WhereAttributeHash, WhereOperators, - ModelCtor, + ModelStatic, Hookable, - ModelType, CreationAttributes, Attributes } from './model'; @@ -768,8 +767,8 @@ export class Sequelize extends Hooks { * @param name * @param fn A callback function that is called with factory */ - public static afterDefine(name: string, fn: (model: ModelType) => void): void; - public static afterDefine(fn: (model: ModelType) => void): void; + public static afterDefine(name: string, fn: (model: ModelStatic) => void): void; + public static afterDefine(fn: (model: ModelStatic) => void): void; /** * A hook that is run before Sequelize() call @@ -859,7 +858,7 @@ export class Sequelize extends Hooks { * Dictionary of all models linked with this instance. */ public readonly models: { - [key: string]: ModelCtor; + [key: string]: ModelStatic; }; /** @@ -1083,8 +1082,8 @@ export class Sequelize extends Hooks { * @param name * @param fn A callback function that is called with factory */ - public afterDefine(name: string, fn: (model: ModelType) => void): void; - public afterDefine(fn: (model: ModelType) => void): void; + public afterDefine(name: string, fn: (model: ModelStatic) => void): void; + public afterDefine(fn: (model: ModelStatic) => void): void; /** * A hook that is run before Sequelize() call @@ -1209,14 +1208,14 @@ export class Sequelize extends Hooks { modelName: string, attributes: ModelAttributes, options?: ModelOptions - ): ModelCtor; + ): ModelStatic; /** * Fetch a Model which is already defined * * @param modelName The name of a model defined with Sequelize.define */ - public model(modelName: string): ModelCtor; + public model(modelName: string): ModelStatic; /** * Checks whether a model with the given name is defined diff --git a/src/utils.d.ts b/src/utils.d.ts index 4001922990fa..8aa3bd1e5000 100644 --- a/src/utils.d.ts +++ b/src/utils.d.ts @@ -1,5 +1,5 @@ import { DataType } from './data-types'; -import { Model, ModelCtor, ModelType, WhereOptions, Attributes } from './model'; +import { Model, ModelStatic, WhereOptions, Attributes } from './model'; import { Optional } from './index'; export type Primitive = 'string' | 'number' | 'boolean'; @@ -35,17 +35,17 @@ export interface OptionsForMapping { /** Expand and normalize finder options */ export function mapFinderOptions>>( options: T, - model: ModelCtor + model: ModelStatic ): T; /* Used to map field names in attributes and where conditions */ export function mapOptionFieldNames>>( - options: T, model: ModelCtor + options: T, model: ModelStatic ): T; -export function mapWhereFieldNames(attributes: object, model: ModelType): object; +export function mapWhereFieldNames(attributes: object, model: ModelStatic): object; /** Used to map field names in values */ -export function mapValueFieldNames(dataValues: object, fields: string[], model: ModelType): object; +export function mapValueFieldNames(dataValues: object, fields: string[], model: ModelStatic): object; export function isColString(value: string): boolean; export function canTreatArrayAsAnd(arr: unknown[]): boolean; diff --git a/test/types/model.ts b/test/types/model.ts index 1bf7579c93b6..b8cc05f52a91 100644 --- a/test/types/model.ts +++ b/test/types/model.ts @@ -7,10 +7,9 @@ import { Model, Optional, Sequelize, - ModelDefined, CreationOptional, InferAttributes, - InferCreationAttributes, + InferCreationAttributes, ModelStatic, } from 'sequelize'; expectTypeOf().toMatchTypeOf(); @@ -240,10 +239,10 @@ interface ModelAttributes { interface CreationAttributes extends Optional {} -const ModelWithAttributes: ModelDefined< +const ModelWithAttributes: ModelStatic = sequelize.define('efs', { +>> = sequelize.define('efs', { name: DataTypes.STRING }); diff --git a/test/types/sequelize.ts b/test/types/sequelize.ts index 1c7ace9fea7b..cfaac462b7d5 100644 --- a/test/types/sequelize.ts +++ b/test/types/sequelize.ts @@ -1,4 +1,4 @@ -import { Config, Sequelize, Model, QueryTypes, ModelCtor, Op, Utils } from 'sequelize'; +import { Config, Sequelize, Model, QueryTypes, ModelStatic, Op, Utils } from 'sequelize'; Sequelize.useCLS({ get(key: string): unknown { @@ -79,7 +79,7 @@ const rnd: Utils.Fn = sequelize.random(); class Model1 extends Model{} class Model2 extends Model{} -const myModel: ModelCtor = sequelize.models.asd; +const myModel: ModelStatic = sequelize.models.asd; myModel.hasOne(Model2) myModel.findAll(); diff --git a/test/types/type-helpers/deep-writable.ts b/test/types/type-helpers/deep-writable.ts index 29406bc3d3b0..f582d7775f9d 100644 --- a/test/types/type-helpers/deep-writable.ts +++ b/test/types/type-helpers/deep-writable.ts @@ -9,8 +9,6 @@ import { Model, Sequelize, - ModelCtor, - ModelDefined, ModelStatic, } from 'sequelize'; @@ -31,8 +29,6 @@ type SequelizeBasic = | Builtin | Sequelize | Model - | ModelCtor - | ModelDefined | ModelStatic; // type ToMutableArrayIfNeeded = T extends readonly any[] diff --git a/test/types/typescriptDocs/ModelInit.ts b/test/types/typescriptDocs/ModelInit.ts index ea7ebe6cd6e3..3b4108e707f0 100644 --- a/test/types/typescriptDocs/ModelInit.ts +++ b/test/types/typescriptDocs/ModelInit.ts @@ -8,7 +8,7 @@ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, - HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelStatic, Optional, Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from 'sequelize'; @@ -165,10 +165,10 @@ interface NoteAttributes { type NoteCreationAttributes = Optional; // And with a functional approach defining a module looks like this -const Note: ModelDefined< +const Note: ModelStatic = sequelize.define( +>> = sequelize.define( 'Note', { id: { From 4f162534c49c5e012a228360c7d4fde915296547 Mon Sep 17 00:00:00 2001 From: ephys Date: Fri, 11 Feb 2022 00:59:11 +0100 Subject: [PATCH 2/5] feat: simplify ts model definition --- docs/manual/other-topics/typescript.md | 342 ++++++++---------- docs/manual/other-topics/upgrade-to-v7.md | 95 +++++ src/hooks.d.ts | 104 +++--- src/index.d.ts | 2 + src/model.d.ts | 104 ++++-- src/sequelize.d.ts | 16 +- src/utils/typing.ts | 22 ++ test/types/attributes.ts | 39 +- test/types/define.ts | 10 +- test/types/hooks.ts | 8 +- test/types/infer-attributes.ts | 7 +- test/types/model.ts | 43 +-- test/types/models/User.ts | 14 +- test/types/models/UserGroup.ts | 9 +- test/types/models/UserPost.ts | 13 +- test/types/typescriptDocs/Define.ts | 20 +- .../typescriptDocs/DefineNoAttributes.ts | 32 -- test/types/typescriptDocs/ModelInit.ts | 81 ++--- .../typescriptDocs/ModelInitNoAttributes.ts | 49 --- test/types/upsert.ts | 6 +- 20 files changed, 494 insertions(+), 522 deletions(-) create mode 100644 src/utils/typing.ts delete mode 100644 test/types/typescriptDocs/DefineNoAttributes.ts delete mode 100644 test/types/typescriptDocs/ModelInitNoAttributes.ts diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index cde929b3dee2..bf1530dd1862 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -15,86 +15,34 @@ In order to avoid installation bloat for non TS users, you must install the foll - `@types/node` (this is universally required in node projects) - `@types/validator` -## Usage +## Recommended 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](./model-basics.html#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; - -class User extends Model { - declare id: number; - declare string: number; - // 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, 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: +Here is an example of a minimal TypeScript project with strict type-checking for attributes: [//]: # (NOTE for maintainers: Keep the following code in sync with `/types/test/typescriptDocs/ModelInit.ts` to ensure it typechecks correctly.) ```typescript +/** + * 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, HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, - HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, - Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, + Sequelize, CreationOptional, NonAttribute, ModelDefined, } 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> { +class User extends Model { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare name: string; @@ -108,8 +56,8 @@ class User extends Model, InferCreat // 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! + // these will not exist until `Model.init` has been called. + declare getProjects: HasManyGetAssociationsMixin; declare addProject: HasManyAddAssociationMixin; declare addProjects: HasManyAddAssociationsMixin; declare setProjects: HasManySetAssociationsMixin; @@ -122,7 +70,8 @@ class User extends Model, InferCreat // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + // Note: this is optional since it's only populated when explicitly requested in code + declare projects?: NonAttribute; // getters that are not attributes should be tagged using NonAttribute // to remove them from the model's Attribute Typings. @@ -135,10 +84,7 @@ class User extends Model, InferCreat }; } -class Project extends Model< - InferAttributes, - InferCreationAttributes -> { +class Project extends Model { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare ownerId: number; @@ -154,10 +100,7 @@ class Project extends Model< declare updatedAt: CreationOptional; } -class Address extends Model< - InferAttributes
, - InferCreationAttributes
-> { +class Address extends Model
{ declare userId: number; declare address: string; @@ -172,23 +115,23 @@ 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 + allowNull: false, }, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, }, { sequelize, - tableName: 'projects' - } + tableName: 'projects', + }, ); User.init( @@ -196,84 +139,78 @@ 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 - } + 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 - } + sequelize, // passing the `sequelize` instance is required + }, ); // You can also define modules in a functional way interface NoteAttributes { - id: number; - title: string; + id: CreationOptional; + title: CreationOptional; 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( +const Note: ModelDefined = sequelize.define( '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`! + as: 'projects', // this determines the name in `associations`! }); Address.belongsTo(User, { targetKey: 'id' }); @@ -287,12 +224,12 @@ async function doStuffWithUser() { 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 @@ -306,84 +243,26 @@ async function doStuffWithUser() { })(); ``` -### 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. - -[//]: # (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"; - -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); - -class User extends Model { - declare id: number; - declare name: string; - declare preferredName: string | null; -} - -User.init( - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: true, - primaryKey: true, - }, - name: { - type: new DataTypes.STRING(128), - allowNull: false, - }, - preferredName: { - type: new DataTypes.STRING(128), - allowNull: true, - }, - }, - { - tableName: "users", - sequelize, // passing the `sequelize` instance is required - } -); - -async function doStuffWithUserModel() { - const newUser = await User.create({ - name: "Johnny", - preferredName: "John", - }); - console.log(newUser.id, newUser.name, newUser.preferredName); - - const foundUser = await User.findOne({ where: { name: "Johnny" } }); - if (foundUser === null) return; - console.log(foundUser.name); -} -``` - ## 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. [//]: # (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"; +```typescript +import { Sequelize, Model, DataTypes, CreationOptional } from 'sequelize'; -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // We recommend you declare an interface for the attributes, for stricter typechecking -interface UserAttributes { - id: number; +// We need to declare an interface for our model that is basically what our class would be +interface IUserModel extends Model { + // Some fields are optional when calling UserModel.create() or UserModel.build() + id: CreationOptional; name: string; } -// Some fields are optional when calling UserModel.create() or UserModel.build() -interface UserCreationAttributes extends Optional {} - -// We need to declare an interface for our model that is basically what our class would be -interface UserInstance - extends Model, - UserAttributes {} - -const UserModel = sequelize.define("User", { +const UserModel = sequelize.define('User', { id: { primaryKey: true, type: DataTypes.INTEGER.UNSIGNED, @@ -401,39 +280,112 @@ 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. +## Type inference for Attributes -[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/DefineNoAttributes.ts` to ensure it typechecks correctly.) +Sequelize needs to be aware of the proper typing of your attributes, to ensure methods +like `Model.create`, `Model.findAll`, etc… are typed correctly. -```ts -import { Sequelize, Model, DataTypes } from "sequelize"; +In Sequelize 6, you had to write those typings by hand and pass them as generic to `Model` -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); +Starting with Sequelize 7, Sequelize will infer your Model's attribute typings for +you using your class's public class fields! +It will consider any declared public class field to be an attribute except: -// We need to declare an interface for our model that is basically what our class would be -interface UserInstance extends Model { - id: number; - name: string; +- Static fields and methods. +- Methods (anything whose type is a function). +- Fields whose type uses the branded type `NonAttribute`. + ```typescript + import { NonAttribute, Model } from 'sequelize'; + + class User extends Model { + // 'id' is considered to be an attribute + declare id: number; + + // 'projects' is not considered to be an attribute, + // because its type uses 'NonAttribute'. + declare projects?: NonAttribute; + } + ``` +- Those excluded by the 'omit' generic option. + ```typescript + import { Model } from 'sequelize'; + + class User extends Model { + // 'id' is considered to be an attribute + declare id: number; + + // 'projects' is not considered to be an attribute, + // because it has been added to the 'omit' option + declare projects?: Project[]; + // 'address' is not considered to be an attribute + // because it has been added to the 'omit' option + declare address?: Address; + } + ``` +- Those inherited from Sequelize's `Model` class (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. + +**Note**: Getter & setters are not automatically excluded. This is because it's not possible to know whether a property is a getter or not through typings. + +Set their return / parameter type to `NonAttribute`, or add them to `omit` to exclude them: + +```typescript +import { Model, NonAttribute } from 'sequelize'; + +class User extends Model { + // 'firstName' is considered to be an attribute + declare firstName: string; + + // you need to set its return value to NonAttribute<> + // or sequelize will consider this to be an attribute. + get name(): NonAttribute { + return this.firstName; + } } -const UserModel = sequelize.define("User", { - id: { - primaryKey: true, - type: DataTypes.INTEGER.UNSIGNED, - }, - name: { - type: DataTypes.STRING, - }, -}); +// or -async function doStuff() { - const instance = await UserModel.findByPk(1, { - rejectOnEmpty: true, - }); - console.log(instance.id); +// you can exclude the getter using 'omit': +class User extends Model { + declare firstName: string; + + get name(): string { + return this.firstName; + } } ``` +### Attributes optional during creation + +Some attributes have default values, and are therefore optional when creating a new instance of your model. + +The attribute typing inference will need a bit of help to properly mark these attributes as optional. + +Here is a scenario where TypeScript will complain even though it is valid code: + +```typescript +class User extends Model { + declare id: number; +} + +// Typing Error! 'id' is missing, because TypeScript doesn't know that 'id' is optional. +await User.create({}); +``` + +You fix that using the `CreationOptional` type. This type tells Sequelize +that the attribute is optional during model creation: + +```typescript +class User extends Model { + declare id: CreationOptional; +} + +// No error! Since `id` has been marked as optional when creating an instance with `CreationOptional`. +await User.create({}); +``` + + ## Utility Types ### Requesting a Model Class diff --git a/docs/manual/other-topics/upgrade-to-v7.md b/docs/manual/other-topics/upgrade-to-v7.md index 91243ebdf047..2ba338d61a71 100644 --- a/docs/manual/other-topics/upgrade-to-v7.md +++ b/docs/manual/other-topics/upgrade-to-v7.md @@ -2,6 +2,15 @@ Sequelize v7 is the next major release after v6. Below is a list of breaking changes to help you upgrade. +## Highlights + +Sequelize 7 is a release with lots of breaking changes, but it is also filled with improvements and new features! + +### Simplified TypeScript integration + +Take a look at our [updated TypeScript guide](https://sequelize.org/v7/manual/typescript.html). +We hope you'll find that integrating Sequelize with TypeScript in v7 is much easier than in v6. + ## Breaking Changes ### Support for Node 12 and up @@ -16,6 +25,92 @@ As a result, the manual typings that were formerly best-effort guesses on top of have been removed and all typings are now directly retrieved from the actual TypeScript code. You'll likely find many tiny differences which however should be easy to fix. +#### Changes to class-based Model typings + +Typings for Models have been greatly simplified. Instead of having to write something like the following: + +```typescript +import { Model, Optional } from 'sequelize'; + +type UserAttributes = { + id: number; + firstName: string; +}; + +type UserCreationAttributes = Optional; + +class User extends Model { + declare id: number; + declare firstName: string; +} +``` + +You instead need to write it like this: + +```typescript +import { Model, CreationOptional } from 'sequelize'; + +class User extends Model { + // attribute 'id' has a default value, it can be omitted from Model.create and other creation methods + // `CreationOptional` marks it as such in the typings. + declare id: CreationOptional; + declare firstName: string; +} +``` + +We recommend you take a look at [the updated TypeScript guide for Sequelize](https://sequelize.org/v7/manual/typescript.html). + +Note: If you were already using `InferAttributes` & `InferCreationAttributes` in Sequelize 6, +then your code already looked like the one above. Simply change how you extend the Model class +from `Model, InferCreationAttributes>` to `Model` + +#### Changes to `sequelize.define`-based model typings + +`ModelDefined` has been updated to match how the `Model` class works. + +Instead of writing this: + +```typescript +import { ModelDefined, Optional } from 'sequelize'; + +type UserAttributes = { + id: number; + firstName: string; +}; + +type UserCreationAttributes = Optional; + +const User: ModelDefined = sequelize.define('User', { + id: DataTypes.INTEGER, + firstName: DataTypes.STRING, +}); +``` + +You need to write it like this: + +```typescript +import { ModelDefined, CreationOptional } from 'sequelize'; + +type UserAttributes = { + // attribute 'id' has a default value, it can be omitted from Model.create and other creation methods + // `CreationOptional` marks it as such in the typings. + id: CreationOptional; + firstName: string; +}; + +const User: ModelDefined = sequelize.define('User', { + id: DataTypes.INTEGER, + firstName: DataTypes.STRING, +}); +``` + +#### Changes to Static Model typings + +We improved the typing of `ModelStatic` in Sequelize v6. It now fulfils the role that was previously split +between `ModelCtor`, `ModelType`, and `ModelStatic`. + +It is typically safe to simply replace any use of those with `ModelStatic`. + ### Changes to `ConnectionManager` *This only impacts you if you used `ConnectionManager` directly.* diff --git a/src/hooks.d.ts b/src/hooks.d.ts index 34f6e8850cd7..2c49591d3f28 100644 --- a/src/hooks.d.ts +++ b/src/hooks.d.ts @@ -9,7 +9,7 @@ import Model, { InstanceUpdateOptions, ModelAttributes, ModelOptions, RestoreOptions, UpdateOptions, UpsertOptions, - Attributes, CreationAttributes, ModelStatic + Attributes, CreationAttributes, ModelStatic, } from './model'; import { AbstractQuery } from './dialects/abstract/query'; import { QueryOptions } from './dialects/abstract/query-interface'; @@ -22,40 +22,40 @@ export type HookReturn = Promise | void; * Options for Model.init. We mostly duplicate the Hooks here, since there is no way to combine the two * interfaces. */ -export interface ModelHooks { +export interface ModelHooks { beforeValidate(instance: M, options: ValidationOptions): HookReturn; afterValidate(instance: M, options: ValidationOptions): HookReturn; - beforeCreate(attributes: M, options: CreateOptions): HookReturn; - afterCreate(attributes: M, options: CreateOptions): HookReturn; + beforeCreate(attributes: M, options: CreateOptions>): HookReturn; + afterCreate(attributes: M, options: CreateOptions>): HookReturn; beforeDestroy(instance: M, options: InstanceDestroyOptions): HookReturn; afterDestroy(instance: M, options: InstanceDestroyOptions): HookReturn; beforeRestore(instance: M, options: InstanceRestoreOptions): HookReturn; afterRestore(instance: M, options: InstanceRestoreOptions): HookReturn; - beforeUpdate(instance: M, options: InstanceUpdateOptions): HookReturn; - afterUpdate(instance: M, options: InstanceUpdateOptions): HookReturn; - beforeUpsert(attributes: M, options: UpsertOptions): HookReturn; - afterUpsert(attributes: [ M, boolean | null ], options: UpsertOptions): HookReturn; + beforeUpdate(instance: M, options: InstanceUpdateOptions>): HookReturn; + afterUpdate(instance: M, options: InstanceUpdateOptions>): HookReturn; + beforeUpsert(attributes: M, options: UpsertOptions>): HookReturn; + afterUpsert(attributes: [ M, boolean | null ], options: UpsertOptions>): HookReturn; beforeSave( instance: M, - options: InstanceUpdateOptions | CreateOptions + options: InstanceUpdateOptions> | CreateOptions> ): HookReturn; afterSave( instance: M, - options: InstanceUpdateOptions | CreateOptions + options: InstanceUpdateOptions> | CreateOptions> ): HookReturn; - beforeBulkCreate(instances: M[], options: BulkCreateOptions): HookReturn; - afterBulkCreate(instances: readonly M[], options: BulkCreateOptions): HookReturn; - beforeBulkDestroy(options: DestroyOptions): HookReturn; - afterBulkDestroy(options: DestroyOptions): HookReturn; - beforeBulkRestore(options: RestoreOptions): HookReturn; - afterBulkRestore(options: RestoreOptions): HookReturn; - beforeBulkUpdate(options: UpdateOptions): HookReturn; - afterBulkUpdate(options: UpdateOptions): HookReturn; - beforeFind(options: FindOptions): HookReturn; - beforeCount(options: CountOptions): HookReturn; - beforeFindAfterExpandIncludeAll(options: FindOptions): HookReturn; - beforeFindAfterOptions(options: FindOptions): HookReturn; - afterFind(instancesOrInstance: readonly M[] | M | null, options: FindOptions): HookReturn; + beforeBulkCreate(instances: M[], options: BulkCreateOptions>): HookReturn; + afterBulkCreate(instances: readonly M[], options: BulkCreateOptions>): HookReturn; + beforeBulkDestroy(options: DestroyOptions>): HookReturn; + afterBulkDestroy(options: DestroyOptions>): HookReturn; + beforeBulkRestore(options: RestoreOptions>): HookReturn; + afterBulkRestore(options: RestoreOptions>): HookReturn; + beforeBulkUpdate(options: UpdateOptions>): HookReturn; + afterBulkUpdate(options: UpdateOptions>): HookReturn; + beforeFind(options: FindOptions>): HookReturn; + beforeCount(options: CountOptions>): HookReturn; + beforeFindAfterExpandIncludeAll(options: FindOptions>): HookReturn; + beforeFindAfterOptions(options: FindOptions>): HookReturn; + afterFind(instancesOrInstance: readonly M[] | M | null, options: FindOptions>): HookReturn; beforeSync(options: SyncOptions): HookReturn; afterSync(options: SyncOptions): HookReturn; beforeBulkSync(options: SyncOptions): HookReturn; @@ -66,11 +66,9 @@ export interface ModelHooks { export interface SequelizeHooks< - M extends Model = Model, - TAttributes = any, - TCreationAttributes = TAttributes -> extends ModelHooks { - beforeDefine(attributes: ModelAttributes, options: ModelOptions): void; + M extends Model = Model, +> extends ModelHooks { + beforeDefine(attributes: ModelAttributes>, options: ModelOptions): void; afterDefine(model: ModelStatic): void; beforeInit(config: Config, options: Options): void; afterInit(sequelize: Sequelize): void; @@ -83,33 +81,13 @@ export interface SequelizeHooks< /** * Virtual class for deduplication */ -export class Hooks< - M extends Model = Model, - TModelAttributes extends {} = any, - TCreationAttributes extends {} = TModelAttributes -> { +export class Hooks { /** * A dummy variable that doesn't exist on the real object. This exists so * Typescript can infer the type of the attributes in static functions. Don't * try to access this! */ _model: M; - /** - * 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; // 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; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) /** * Add a hook to the model @@ -119,20 +97,20 @@ export class Hooks< */ public static addHook< H extends Hooks, - K extends keyof SequelizeHooks, CreationAttributes> + K extends keyof SequelizeHooks >( this: HooksStatic, hookType: K, name: string, - fn: SequelizeHooks, CreationAttributes>[K] + fn: SequelizeHooks[K] ): HooksCtor; public static addHook< H extends Hooks, - K extends keyof SequelizeHooks, CreationAttributes> + K extends keyof SequelizeHooks >( this: HooksStatic, hookType: K, - fn: SequelizeHooks, CreationAttributes>[K] + fn: SequelizeHooks[K] ): HooksCtor; /** @@ -140,7 +118,7 @@ export class Hooks< */ public static removeHook( this: HooksStatic, - hookType: keyof SequelizeHooks, CreationAttributes>, + hookType: keyof SequelizeHooks, name: string, ): HooksCtor; @@ -149,11 +127,11 @@ export class Hooks< */ public static hasHook( this: HooksStatic, - hookType: keyof SequelizeHooks, CreationAttributes>, + hookType: keyof SequelizeHooks, ): boolean; public static hasHooks( this: HooksStatic, - hookType: keyof SequelizeHooks, CreationAttributes>, + hookType: keyof SequelizeHooks, ): boolean; /** @@ -162,17 +140,17 @@ export class Hooks< * @param name Provide a name for the hook function. It can be used to remove the hook later or to order * hooks based on some sort of priority system in the future. */ - public addHook>( + public addHook>( hookType: K, name: string, - fn: SequelizeHooks[K] + fn: SequelizeHooks[K] ): this; - public addHook>( - hookType: K, fn: SequelizeHooks[K]): this; + public addHook>( + hookType: K, fn: SequelizeHooks[K]): this; /** * Remove hook from the model */ - public removeHook>( + public removeHook>( hookType: K, name: string ): this; @@ -180,8 +158,8 @@ export class Hooks< /** * Check whether the mode has any hooks of this type */ - public hasHook>(hookType: K): boolean; - public hasHooks>(hookType: K): boolean; + public hasHook>(hookType: K): boolean; + public hasHooks>(hookType: K): boolean; } export type HooksCtor = typeof Hooks & { new(): H }; diff --git a/src/index.d.ts b/src/index.d.ts index d365c8375c56..f830e3373b6d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -27,3 +27,5 @@ export { Utils, DataTypes, Deferrable }; export type Optional = Omit & Partial>; export type PartlyRequired = Omit & Required>; +export { NonConstructorKeys } from './utils/typing.js'; +export { OmitConstructors } from './utils/typing.js'; diff --git a/src/model.d.ts b/src/model.d.ts index 8b5117a19ba3..c275a9768a56 100644 --- a/src/model.d.ts +++ b/src/model.d.ts @@ -1,14 +1,25 @@ import { IndexHints } from './index-hints'; -import { Association, BelongsTo, BelongsToMany, BelongsToManyOptions, BelongsToOptions, HasMany, HasManyOptions, HasOne, HasOneOptions } from './associations/index'; +import { + Association, + BelongsTo, + BelongsToMany, + BelongsToManyOptions, + BelongsToOptions, + HasMany, + HasManyOptions, + HasOne, + HasOneOptions, +} from './associations/index'; import { DataType } from './data-types'; import { Deferrable } from './deferrable'; import { HookReturn, Hooks, ModelHooks } from './hooks'; import { ValidationOptions } from './instance-validator'; import { IndexesOptions, QueryOptions, TableName } from './dialects/abstract/query-interface'; import { Sequelize, SyncOptions } from './sequelize'; -import { Col, Fn, Literal, Where, MakeUndefinedOptional, AnyFunction } from './utils'; -import { LOCK, Transaction, Op } from './index'; +import { AnyFunction, Col, Fn, Literal, MakeUndefinedOptional, Where } from './utils'; +import { LOCK, Op, Transaction } from './index'; import { SetRequired } from './utils/set-required'; +import { IsBranded, OmitConstructors } from './utils/typing'; export interface Logging { /** @@ -1550,7 +1561,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 @@ -1601,9 +1612,39 @@ export interface AddScopeOptions { override: boolean; } -export abstract class Model - extends Hooks, TModelAttributes, TCreationAttributes> -{ +/** + * Do not export this! + * + * This is a type-only value that exposes Attributes on {@link ModelInternal}. + * Used by {@link Attributes} + */ +declare const AttributeSymbol: unique symbol; + +/** + * Do not export this! + * + * This is a type-only value that exposes Creation Attributes on {@link ModelInternal} + * Used by {@link CreationAttributes} + */ +declare const CreationAttributeSymbol: unique symbol; + +export abstract class Model< + M extends ModelInternal = any, + Opts extends InferAttributesOptions = { omit: never } +> extends ModelInternal, InferCreationAttributes> {} + +/** + * Do not export this! + * + * ModelInternal is like {@link Model}, with the difference that its generics + * expect the attributes of the model, instead of the model itself. + * + * Do not use this class directly! Use {@link Model} for most use cases, and {@link ModelDefined} to type the output of {@link Sequelize#define}. + */ +declare abstract class ModelInternal< + TModelAttributes extends {} = any, + TCreationAttributes extends {} = TModelAttributes +> extends Hooks>> { /** * A dummy variable that doesn't exist on the real object. This exists so * Typescript can infer the type of the attributes in static functions. Don't @@ -1622,7 +1663,7 @@ export abstract class Model instead of this property to be forward-compatible. */ - _attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in hooks.d.ts) + [AttributeSymbol]: TModelAttributes; /** * A similar dummy variable that doesn't exist on the real object. Do not @@ -1631,7 +1672,7 @@ export abstract class Model instead of this property to be forward-compatible. */ - _creationAttributes: TCreationAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in hooks.d.ts) + [CreationAttributeSymbol]: TCreationAttributes; /** The name of the database table */ public static readonly tableName: string; @@ -2990,26 +3031,17 @@ export abstract class Model = ({[P in keyof T]: T[P] extends new () => any ? never : P })[keyof T]; -type NonConstructor = Pick>; - // remove the existing constructor that tries to return `Model<{},{}>` which would be incompatible with models that have typing defined & replace with proper constructor. -export type ModelStatic = NonConstructor & { new(): M }; - -export default Model; +export type ModelStatic = OmitConstructors & { new(): M }; /** - * Type will be true is T is branded with Brand, false otherwise + * ModelDefined represents a Model as generated by {@link Sequelize#define}. */ -// How this works: -// - `A extends B` will be true if A has *at least* all the properties of B -// - If we do `A extends Omit` - the result will only be true if A did not have Checked to begin with -// - So if we want to check if T is branded, we remove the brand, and check if they list of keys is still the same. -// we exclude Null & Undefined so "field: Brand | null" is still detected as branded -// this is important because "Brand" are transformed into "Brand | null" to not break null & undefined -type IsBranded = keyof NonNullable extends keyof Omit, Brand> - ? false - : true; +export type ModelDefined = ModelStatic>; + +export default Model; + + /** * Dummy Symbol used as branding by {@link NonAttribute}. @@ -3036,7 +3068,7 @@ export type NonAttribute = * * - omit: properties to not treat as Attributes. */ -type InferAttributesOptions = { omit?: Excluded }; +type InferAttributesOptions = { omit?: Excluded }; /** * Utility type to extract Attributes of a given Model class. @@ -3083,10 +3115,10 @@ type InferAttributesOptions = { omit?: Excluded }; * projects?: NonAttribute; * } */ -export type InferAttributes< +type InferAttributes< M extends Model, Options extends InferAttributesOptions = { omit: never } - > = { +> = { [Key in keyof M as InternalInferAttributeKeysFromFields]: M[Key] }; @@ -3125,10 +3157,10 @@ export type CreationOptional = * declare name: string; * } */ -export type InferCreationAttributes< +type InferCreationAttributes< M extends Model, Options extends InferAttributesOptions = { omit: never } - > = { +> = { [Key in keyof M as InternalInferAttributeKeysFromFields]: IsBranded extends true ? (M[Key] | undefined) : M[Key] @@ -3155,8 +3187,6 @@ type InternalInferAttributeKeysFromFields(modelClass: ModelStatic, attributes: CreationAttributes) {} */ -export type CreationAttributes = MakeUndefinedOptional; +export type CreationAttributes> = + M extends ModelStatic ? MakeUndefinedOptional + : M extends Model ? MakeUndefinedOptional + : never; /** * Returns the creation attributes of a given Model. @@ -3177,4 +3210,7 @@ export type CreationAttributes = MakeUndefinedOptional< * @example * function getValue(modelClass: ModelStatic, attribute: keyof Attributes) {} */ -export type Attributes = M['_attributes']; +export type Attributes> = + M extends ModelStatic ? M2[typeof AttributeSymbol] + : M extends Model ? M[typeof AttributeSymbol] + : never; diff --git a/src/sequelize.d.ts b/src/sequelize.d.ts index 6c5571ada1be..6270b5227e9e 100644 --- a/src/sequelize.d.ts +++ b/src/sequelize.d.ts @@ -377,7 +377,7 @@ export interface Options extends Logging { /** * Sets global permanent hooks. */ - hooks?: Partial>; + hooks?: Partial>; /** * Set to `true` to automatically minify aliases generated by sequelize. @@ -591,11 +591,11 @@ export class Sequelize extends Hooks { * @param name * @param fn A callback function that is called with instance, options */ - public static beforeSave( + public static beforeSave( name: string, - fn: (instance: Model, options: UpdateOptions | CreateOptions) => void + fn: (instance: M, options: UpdateOptions> | CreateOptions>) => void ): void; - public static beforeSave(fn: (instance: Model, options: UpdateOptions | CreateOptions) => void): void; + public static beforeSave(fn: (instance: M, options: UpdateOptions> | CreateOptions>) => void): void; /** * A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate` @@ -603,12 +603,12 @@ export class Sequelize extends Hooks { * @param name * @param fn A callback function that is called with instance, options */ - public static afterSave( + public static afterSave( name: string, - fn: (instance: Model, options: UpdateOptions | CreateOptions) => void + fn: (instance: M, options: UpdateOptions> | CreateOptions>) => void ): void; - public static afterSave( - fn: (instance: Model, options: UpdateOptions | CreateOptions) => void + public static afterSave( + fn: (instance: M, options: UpdateOptions> | CreateOptions>) => void ): void; /** diff --git a/src/utils/typing.ts b/src/utils/typing.ts new file mode 100644 index 000000000000..dd0b54820db7 --- /dev/null +++ b/src/utils/typing.ts @@ -0,0 +1,22 @@ +/** + * Lists all keys that are not constructors + */ +export type NonConstructorKeys = ({ [P in keyof T]: T[P] extends new () => any ? never : P })[keyof T]; + +/** + * Removes all constructors from the T + */ +export type OmitConstructors = Pick>; + +/** + * Type will be true is T is branded with Brand, false otherwise + */ +// How this works: +// - `A extends B` will be true if A has *at least* all the properties of B +// - If we do `A extends Omit` - the result will only be true if A did not have Checked to begin with +// - So if we want to check if T is branded, we remove the brand, and check if they list of keys is still the same. +// we exclude Null & Undefined so "field: Brand | null" is still detected as branded +// this is important because "Brand" are transformed into "Brand | null" to not break null & undefined +export type IsBranded = keyof NonNullable extends keyof Omit, Brand> + ? false + : true; diff --git a/test/types/attributes.ts b/test/types/attributes.ts index 4c2e5b6f4439..fcfd0abee051 100644 --- a/test/types/attributes.ts +++ b/test/types/attributes.ts @@ -1,14 +1,7 @@ -import { Model } from "sequelize"; +import { Attributes, CreationAttributes, Model, ModelStatic } from 'sequelize'; +import { expectTypeOf } from 'expect-type'; -interface UserCreationAttributes { - name: string; -} - -interface UserAttributes extends UserCreationAttributes { - id: number; -} - -class User extends Model implements UserAttributes { +class User extends Model { declare id: number; declare name: string; @@ -25,9 +18,7 @@ interface ProjectAttributes extends ProjectCreationAttributes { id: number; } -class Project - extends Model - implements ProjectAttributes { +class Project extends Model implements ProjectAttributes { declare id: number; declare ownerId: number; declare name: string; @@ -40,3 +31,25 @@ class Address extends Model { // both models should be accepted in include User.findAll({ include: [Project, Address] }); + +expectTypeOf>().toEqualTypeOf<{ + id: number, + name: string, +}>(); + +// test Attributes works on ModelStatic too +expectTypeOf>>().toEqualTypeOf<{ + id: number, + name: string, +}>(); + +expectTypeOf>().toEqualTypeOf<{ + id: number, + name: string, +}>(); + +// test Attributes works on ModelStatic too +expectTypeOf>>().toEqualTypeOf<{ + id: number, + name: string, +}>(); diff --git a/test/types/define.ts b/test/types/define.ts index 54d422c88747..00da263e5193 100644 --- a/test/types/define.ts +++ b/test/types/define.ts @@ -1,19 +1,17 @@ import { expectTypeOf } from 'expect-type'; -import { BuildOptions, DataTypes, Model, Optional } from 'sequelize'; +import { BuildOptions, CreationOptional, DataTypes, Model } from 'sequelize'; import { sequelize } from './connection'; // I really wouldn't recommend this, but if you want you can still use define() and interfaces interface UserAttributes { - id: number; + id: CreationOptional; username: string; firstName: string; lastName: string; } -interface UserCreationAttributes extends Optional {} - -interface UserModel extends Model, UserAttributes {} +interface UserModel extends Model, UserAttributes {} const User = sequelize.define( 'User', @@ -37,7 +35,7 @@ async function test() { await user.save(); } -// The below doesn't define Attribute types, but should still work +// The below doesn't pass itself to Model, but should still work interface UntypedUserModel extends Model, UserAttributes {} type UntypedUserModelStatic = typeof Model & { diff --git a/test/types/hooks.ts b/test/types/hooks.ts index 33c17f4ef8b0..1ec663a8047e 100644 --- a/test/types/hooks.ts +++ b/test/types/hooks.ts @@ -10,11 +10,11 @@ import { SemiDeepWritable } from "./type-helpers/deep-writable"; const hooks: Partial = { beforeSave(m, options) { expectTypeOf(m).toEqualTypeOf(); - expectTypeOf(options).toMatchTypeOf(); // TODO consider `.toEqualTypeOf` instead ? + expectTypeOf(options).toMatchTypeOf(); }, afterSave(m, options) { expectTypeOf(m).toEqualTypeOf(); - expectTypeOf(options).toMatchTypeOf(); // TODO consider `.toEqualTypeOf` instead ? + expectTypeOf(options).toMatchTypeOf(); }, afterFind(m, options) { expectTypeOf(m).toEqualTypeOf(); @@ -22,11 +22,11 @@ import { SemiDeepWritable } from "./type-helpers/deep-writable"; }, beforeUpsert(m, options) { expectTypeOf(m).toEqualTypeOf(); - expectTypeOf(options).toEqualTypeOf(); + expectTypeOf(options).toMatchTypeOf(); }, afterUpsert(m, options) { expectTypeOf(m).toEqualTypeOf<[ TestModel, boolean | null ]>(); - expectTypeOf(options).toEqualTypeOf(); + expectTypeOf(options).toMatchTypeOf(); }, beforeQuery(options, query) { expectTypeOf(options).toEqualTypeOf(); diff --git a/test/types/infer-attributes.ts b/test/types/infer-attributes.ts index ed410c029ebc..48643e2d4f56 100644 --- a/test/types/infer-attributes.ts +++ b/test/types/infer-attributes.ts @@ -3,18 +3,15 @@ import { Attributes, CreationAttributes, CreationOptional, - InferAttributes, - InferCreationAttributes, Model, NonAttribute, } from 'sequelize'; -class Project extends Model> { +class Project extends Model { declare id: number; } -class User extends Model, - InferCreationAttributes> { +class User extends Model { declare optionalAttribute: CreationOptional; declare mandatoryAttribute: string; diff --git a/test/types/model.ts b/test/types/model.ts index b8cc05f52a91..5d62491b4a3d 100644 --- a/test/types/model.ts +++ b/test/types/model.ts @@ -5,11 +5,9 @@ import { DataTypes, HasOne, Model, - Optional, Sequelize, CreationOptional, - InferAttributes, - InferCreationAttributes, ModelStatic, + ModelDefined, Attributes, } from 'sequelize'; expectTypeOf().toMatchTypeOf(); @@ -117,7 +115,7 @@ const model: typeof MyModel = MyModel.init({ /** * Tests for findCreateFind() type. */ -class UserModel extends Model, InferCreationAttributes> { +class UserModel extends Model { declare username: string; declare beta_user: CreationOptional; } @@ -233,21 +231,20 @@ Actor.belongsToMany(Film, { }); interface ModelAttributes { - id: number; + id: CreationOptional; name: string; } -interface CreationAttributes extends Optional {} - -const ModelWithAttributes: ModelStatic> = sequelize.define('efs', { +const ModelWithAttributes: ModelDefined = sequelize.define('efs', { name: DataTypes.STRING }); +expectTypeOf>().toEqualTypeOf(); + const modelWithAttributes = ModelWithAttributes.build(); +expectTypeOf>().toEqualTypeOf(); + /** * Tests for set() type */ @@ -263,35 +260,31 @@ expectTypeOf(modelWithAttributes.previous).parameter(0).toEqualTypeOf(); expectTypeOf(modelWithAttributes.previous).returns.toEqualTypeOf(); expectTypeOf(modelWithAttributes.previous('name')).toEqualTypeOf(); -expectTypeOf(modelWithAttributes.previous()).toEqualTypeOf>(); +expectTypeOf(modelWithAttributes.previous()).toEqualTypeOf>(); /** * Tests for toJson() type */ -interface FilmToJson { - id: number; - name?: string; -} -class FilmModelToJson extends Model implements FilmToJson { - id!: number; - name?: string; +class FilmModelToJson extends Model { + declare id: number; + declare name?: string; } const film = FilmModelToJson.build(); -class FilmModelExtendToJson extends Model implements FilmToJson { - id!: number; - name?: string; +class FilmModelExtendToJson extends Model { + declare id: number; + declare name?: string; - public toJSON() { + toJSON() { return { id: this.id } } } const filmOverrideToJson = FilmModelExtendToJson.build(); const result = film.toJSON(); -expectTypeOf(result).toEqualTypeOf() +expectTypeOf(result).toEqualTypeOf>(); -type FilmNoNameToJson = Omit +type FilmNoNameToJson = Omit, 'name'> const resultDerived = film.toJSON(); expectTypeOf(resultDerived).toEqualTypeOf() diff --git a/test/types/models/User.ts b/test/types/models/User.ts index e2029fb062f7..2fd1b0787970 100644 --- a/test/types/models/User.ts +++ b/test/types/models/User.ts @@ -1,25 +1,19 @@ import { - InferAttributes, + Attributes, BelongsTo, BelongsToCreateAssociationMixin, BelongsToGetAssociationMixin, BelongsToSetAssociationMixin, - InferCreationAttributes, CreationOptional, DataTypes, FindOptions, Model, ModelStatic, - Op + Op, } from 'sequelize'; import { sequelize } from '../connection'; -type NonUserAttributes = 'group'; - -export class User extends Model< - InferAttributes, - InferCreationAttributes -> { +export class User extends Model { public static associations: { group: BelongsTo; }; @@ -105,7 +99,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; }); diff --git a/test/types/models/UserGroup.ts b/test/types/models/UserGroup.ts index de19a7bec18c..3e678efa01aa 100644 --- a/test/types/models/UserGroup.ts +++ b/test/types/models/UserGroup.ts @@ -1,6 +1,4 @@ import { - InferAttributes, - InferCreationAttributes, CreationOptional, DataTypes, HasMany, @@ -23,11 +21,8 @@ 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< - InferAttributes, - InferCreationAttributes -> { - public static associations: { +export class UserGroup extends Model { + static associations: { users: HasMany }; diff --git a/test/types/models/UserPost.ts b/test/types/models/UserPost.ts index ab8062498a7f..aaa834300eb6 100644 --- a/test/types/models/UserPost.ts +++ b/test/types/models/UserPost.ts @@ -1,19 +1,12 @@ -import { Model, Optional, DataTypes } from 'sequelize'; +import { Model, DataTypes, CreationOptional } from 'sequelize'; import { sequelize } from '../connection'; -export interface UserPostAttributes { - id: number; +export interface UserPostInstance extends Model { + id: CreationOptional; userId: number; text: string; } -export interface UserPostCreationAttributes - extends Optional {} - -export interface UserPostInstance - extends Model, - UserPostAttributes {} - /** * This is a component defined using `sequelize.define` to ensure that various * functions also work with non-class models, which were the default before diff --git a/test/types/typescriptDocs/Define.ts b/test/types/typescriptDocs/Define.ts index 8ea23694d01c..b5ed16cadcbe 100644 --- a/test/types/typescriptDocs/Define.ts +++ b/test/types/typescriptDocs/Define.ts @@ -4,32 +4,26 @@ * * Don't include this comment in the md file. */ -import { Sequelize, Model, DataTypes, Optional } from 'sequelize'; +import { Sequelize, Model, DataTypes, CreationOptional } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // We recommend you declare an interface for the attributes, for stricter typechecking -interface UserAttributes { - id: number; +// We need to declare an interface for our model that is basically what our class would be +interface IUserModel extends Model { + // Some fields are optional when calling UserModel.create() or UserModel.build() + id: CreationOptional; name: string; } -// Some fields are optional when calling UserModel.create() or UserModel.build() -interface UserCreationAttributes extends Optional {} - -// We need to declare an interface for our model that is basically what our class would be -interface UserInstance - extends Model, - UserAttributes {} - -const UserModel = sequelize.define('User', { +const UserModel = sequelize.define('User', { id: { primaryKey: true, type: DataTypes.INTEGER.UNSIGNED, }, name: { type: DataTypes.STRING, - } + }, }); async function doStuff() { diff --git a/test/types/typescriptDocs/DefineNoAttributes.ts b/test/types/typescriptDocs/DefineNoAttributes.ts deleted file mode 100644 index 8273cad18ff1..000000000000 --- a/test/types/typescriptDocs/DefineNoAttributes.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Keep this file in sync with the code in the "Usage of `sequelize.define`" - * 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'; - -const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); - -// We need to declare an interface for our model that is basically what our class would be -interface UserInstance extends Model { - id: number; - name: string; -} - -const UserModel = sequelize.define('User', { - id: { - primaryKey: true, - type: DataTypes.INTEGER.UNSIGNED, - }, - name: { - type: DataTypes.STRING, - }, -}); - -async function doStuff() { - const instance = await UserModel.findByPk(1, { - rejectOnEmpty: true, - }); - console.log(instance.id); -} diff --git a/test/types/typescriptDocs/ModelInit.ts b/test/types/typescriptDocs/ModelInit.ts index 3b4108e707f0..b157ef935ed0 100644 --- a/test/types/typescriptDocs/ModelInit.ts +++ b/test/types/typescriptDocs/ModelInit.ts @@ -8,14 +8,14 @@ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, - HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelStatic, Optional, - Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, + Sequelize, CreationOptional, NonAttribute, ModelDefined, } 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> { +class User extends Model { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare name: string; @@ -29,8 +29,8 @@ class User extends Model, InferCreat // 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! + // these will not exist until `Model.init` has been called. + declare getProjects: HasManyGetAssociationsMixin; declare addProject: HasManyAddAssociationMixin; declare addProjects: HasManyAddAssociationsMixin; declare setProjects: HasManySetAssociationsMixin; @@ -43,7 +43,8 @@ class User extends Model, InferCreat // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + // Note: this is optional since it's only populated when explicitly requested in code + declare projects?: NonAttribute; // getters that are not attributes should be tagged using NonAttribute // to remove them from the model's Attribute Typings. @@ -56,10 +57,7 @@ class User extends Model, InferCreat }; } -class Project extends Model< - InferAttributes, - InferCreationAttributes -> { +class Project extends Model { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare ownerId: number; @@ -75,10 +73,7 @@ class Project extends Model< declare updatedAt: CreationOptional; } -class Address extends Model< - InferAttributes
, - InferCreationAttributes
-> { +class Address extends Model
{ declare userId: number; declare address: string; @@ -93,23 +88,23 @@ 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 + allowNull: false, }, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, }, { sequelize, - tableName: 'projects' - } + tableName: 'projects', + }, ); User.init( @@ -117,84 +112,78 @@ 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 - } + 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 - } + sequelize, // passing the `sequelize` instance is required + }, ); // You can also define modules in a functional way interface NoteAttributes { - id: number; - title: string; + id: CreationOptional; + title: CreationOptional; 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: ModelStatic> = sequelize.define( +const Note: ModelDefined = sequelize.define( '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`! + as: 'projects', // this determines the name in `associations`! }); Address.belongsTo(User, { targetKey: 'id' }); @@ -208,12 +197,12 @@ async function doStuffWithUser() { 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/test/types/typescriptDocs/ModelInitNoAttributes.ts b/test/types/typescriptDocs/ModelInitNoAttributes.ts deleted file mode 100644 index ff8a5c22a22d..000000000000 --- a/test/types/typescriptDocs/ModelInitNoAttributes.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Keep this file in sync with the code in the "Usage without strict types for - * attributes" section in /docs/manual/other-topics/typescript.md - * - * Don't include this comment in the md file. - */ -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 -} - -User.init( - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: true, - primaryKey: true, - }, - name: { - type: new DataTypes.STRING(128), - allowNull: false, - }, - preferredName: { - type: new DataTypes.STRING(128), - allowNull: true, - }, - }, - { - tableName: 'users', - sequelize, // passing the `sequelize` instance is required - }, -); - -async function doStuffWithUserModel() { - const newUser = await User.create({ - name: 'Johnny', - preferredName: 'John', - }); - console.log(newUser.id, newUser.name, newUser.preferredName); - - const foundUser = await User.findOne({ where: { name: 'Johnny' } }); - if (foundUser === null) return; - console.log(foundUser.name); -} diff --git a/test/types/upsert.ts b/test/types/upsert.ts index 9870b53b83b4..e44072371db7 100644 --- a/test/types/upsert.ts +++ b/test/types/upsert.ts @@ -1,7 +1,9 @@ -import {Model} from "sequelize" +import { CreationOptional, Model } from 'sequelize'; import {sequelize} from './connection'; -class TestModel extends Model<{ foo: string; bar: string }, {}> { +class TestModel extends Model { + declare foo: CreationOptional; + declare bar: CreationOptional; } TestModel.init({ From 21a9225e57f999d4cdb4be6390329ee16cfa10a8 Mon Sep 17 00:00:00 2001 From: ephys Date: Fri, 11 Feb 2022 01:13:34 +0100 Subject: [PATCH 3/5] refactor: fix lint issues --- docs/manual/other-topics/typescript.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index bf1530dd1862..aae34e60cb47 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -294,6 +294,7 @@ It will consider any declared public class field to be an attribute except: - Static fields and methods. - Methods (anything whose type is a function). - Fields whose type uses the branded type `NonAttribute`. + ```typescript import { NonAttribute, Model } from 'sequelize'; @@ -306,7 +307,9 @@ It will consider any declared public class field to be an attribute except: declare projects?: NonAttribute; } ``` + - Those excluded by the 'omit' generic option. + ```typescript import { Model } from 'sequelize'; @@ -322,6 +325,7 @@ It will consider any declared public class field to be an attribute except: declare address?: Address; } ``` + - Those inherited from Sequelize's `Model` class (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. @@ -385,7 +389,6 @@ class User extends Model { await User.create({}); ``` - ## Utility Types ### Requesting a Model Class From e23fd37726af4f88627aeddacfde2cdcf47f9eac Mon Sep 17 00:00:00 2001 From: ephys Date: Fri, 11 Feb 2022 01:28:14 +0100 Subject: [PATCH 4/5] build: update expect-type --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 29b25c70d3e7..69ea069df27b 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "eslint": "8.8.0", "eslint-plugin-jsdoc": "37.8.2", "eslint-plugin-mocha": "10.0.3", - "expect-type": "0.12.0", + "expect-type": "0.13.0", "fast-glob": "3.2.11", "fs-jetpack": "4.3.1", "husky": "7.0.4", diff --git a/yarn.lock b/yarn.lock index 50bff6221430..ea94c0bc4c5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3960,10 +3960,10 @@ expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect-type@0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.12.0.tgz#133534b5e2561158c371e74af63fd8f18a9f3d42" - integrity sha512-IHwziEOjpjXqxQhtOAD5zMiQpGztaEKM4Q8wnwoRN9NIFlnyNHNjRxKWv+18UqRfsqi6vVnZIYFU16ePf+HaqA== +expect-type@0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" + integrity sha512-CclevazQfrqo8EvbLPmP7osnb1SZXkw47XPPvUUpeMz4HuGzDltE7CaIt3RLyT9UQrwVK/LDn+KVcC0hcgjgDg== extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: version "3.0.2" From e64c9ae3accc05d39ebb5c239568a0fcf077d7ec Mon Sep 17 00:00:00 2001 From: ephys Date: Fri, 11 Feb 2022 01:28:34 +0100 Subject: [PATCH 5/5] test: fix typing test --- test/types/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/connection.ts b/test/types/connection.ts index 6ef9882839c7..1578b7732563 100644 --- a/test/types/connection.ts +++ b/test/types/connection.ts @@ -11,7 +11,7 @@ sequelize.afterBulkSync((options: SyncOptions) => { async function test() { expectTypeOf( await sequelize.query('SELECT * FROM `test`', { type: QueryTypes.SELECT }) - ).toEqualTypeOf(); + ).toEqualTypeOf(); expectTypeOf( await sequelize.query('INSERT into test set test=1', { type: QueryTypes.INSERT })