TypeScript
我們正努力讓 Sequelize 在 TypeScript 中更容易使用。 某些部分仍在開發中。我們建議使用 sequelize-typescript 來彌補差距,直到我們的改進準備好發佈為止。
Sequelize 提供自己的 TypeScript 定義。
請注意,僅支援 TypeScript >= 4.1。我們的 TypeScript 支援不遵循 SemVer。我們將至少支援一年的 TypeScript 版本,之後可能會在 SemVer 次要版本中刪除它們。
由於 Sequelize 大量依賴於執行時屬性賦值,因此 TypeScript 無法直接使用。需要相當多的手動類型宣告才能使模型可運作。
安裝
為了避免與不同的 Node 版本衝突,不包含 Node 的類型定義。您必須手動安裝 @types/node
。
使用方式
重要:您必須在類別屬性類型宣告上使用 declare
,以確保 TypeScript 不會發出這些類別屬性。請參閱 公共類別欄位的注意事項
Sequelize 模型接受兩種泛型型別,以定義模型的屬性與建立屬性。
import { Model, Optional } from 'sequelize';
// We don't recommend doing this. Read on for the new way of declaring Model typings.
type UserAttributes = {
id: number;
name: string;
// other attributes...
};
// we're telling the Model that 'id' is optional
// when creating an instance of the model (such as using Model.create()).
type UserCreationAttributes = Optional<UserAttributes, 'id'>;
class User extends Model<UserAttributes, UserCreationAttributes> {
declare id: number;
declare name: string;
// other attributes...
}
這個解決方案很冗長。Sequelize >=6.14.0 提供了新的實用類型,將大幅減少必要的樣板程式碼:InferAttributes
和 InferCreationAttributes
。它們將直接從模型中擷取屬性類型宣告。
import { Model, InferAttributes, InferCreationAttributes, CreationOptional } from 'sequelize';
// order of InferAttributes & InferCreationAttributes is important.
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
// 'CreationOptional' is a special type that marks the field as optional
// when creating an instance of the model (such as using Model.create()).
declare id: CreationOptional<number>;
declare name: string;
// other attributes...
}
關於 InferAttributes
和 InferCreationAttributes
的運作方式,需要了解的重要事項:它們將選擇類別的所有已宣告屬性,除了
- 靜態欄位和方法。
- 方法 (任何類型為函數的內容)。
- 其類型使用品牌類型
NonAttribute
的內容。 - 使用 InferAttributes 排除的內容,例如:
InferAttributes<User, { omit: 'properties' | 'to' | 'omit' }>
。 - 由 Model 超類宣告的內容 (但不是中間類別!)。如果您的某個屬性與
Model
的其中一個屬性共享相同的名稱,請變更其名稱。這樣做很可能會導致問題。 - Getter 和 setter 不會自動排除。將它們的回傳/參數類型設定為
NonAttribute
,或將它們新增至omit
以排除它們。
InferCreationAttributes
的運作方式與 InferAttributes
相同,但有一個例外:屬性使用 CreationOptional
類型宣告的類型將被標示為選用。請注意,接受 null
或 undefined
的屬性不需要使用 CreationOptional
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
declare firstName: string;
// there is no need to use CreationOptional on lastName because nullable attributes
// are always optional in User.create()
declare lastName: string | null;
}
// ...
await User.create({
firstName: 'Zoé',
// last name omitted, but this is still valid!
});
您只需要在類別實例欄位或 getter 上使用 CreationOptional
和 NonAttribute
。
一個使用嚴格類型檢查屬性的最小 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, ForeignKey,
} 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<InferAttributes<User, { omit: 'projects' }>, InferCreationAttributes<User, { omit: 'projects' }>> {
// id can be undefined during creation when using `autoIncrement`
declare id: CreationOptional<number>;
declare name: string;
declare preferredName: string | null; // for nullable fields
// timestamps!
// createdAt can be undefined during creation
declare createdAt: CreationOptional<Date>;
// updatedAt can be undefined during creation
declare updatedAt: CreationOptional<Date>;
// 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<Project>; // Note the null assertions!
declare addProject: HasManyAddAssociationMixin<Project, number>;
declare addProjects: HasManyAddAssociationsMixin<Project, number>;
declare setProjects: HasManySetAssociationsMixin<Project, number>;
declare removeProject: HasManyRemoveAssociationMixin<Project, number>;
declare removeProjects: HasManyRemoveAssociationsMixin<Project, number>;
declare hasProject: HasManyHasAssociationMixin<Project, number>;
declare hasProjects: HasManyHasAssociationsMixin<Project, number>;
declare countProjects: HasManyCountAssociationsMixin;
declare createProject: HasManyCreateAssociationMixin<Project, 'ownerId'>;
// You can also pre-declare possible inclusions, these will only be populated if you
// actively include a relation.
declare projects?: NonAttribute<Project[]>; // 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<string> {
return this.name;
}
declare static associations: {
projects: Association<User, Project>;
};
}
class Project extends Model<
InferAttributes<Project>,
InferCreationAttributes<Project>
> {
// id can be undefined during creation when using `autoIncrement`
declare id: CreationOptional<number>;
// foreign keys are automatically added by associations methods (like Project.belongsTo)
// by branding them using the `ForeignKey` type, `Project.init` will know it does not need to
// display an error if ownerId is missing.
declare ownerId: ForeignKey<User['id']>;
declare name: string;
// `owner` is an eagerly-loaded association.
// We tag it as `NonAttribute`
declare owner?: NonAttribute<User>;
// createdAt can be undefined during creation
declare createdAt: CreationOptional<Date>;
// updatedAt can be undefined during creation
declare updatedAt: CreationOptional<Date>;
}
class Address extends Model<
InferAttributes<Address>,
InferCreationAttributes<Address>
> {
declare userId: ForeignKey<User['id']>;
declare address: string;
// createdAt can be undefined during creation
declare createdAt: CreationOptional<Date>;
// updatedAt can be undefined during creation
declare updatedAt: CreationOptional<Date>;
}
Project.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
name: {
type: new DataTypes.STRING(128),
allowNull: false
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{
sequelize,
tableName: 'projects'
}
);
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
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{
tableName: 'users',
sequelize // passing the `sequelize` instance is required
}
);
Address.init(
{
address: {
type: new DataTypes.STRING(128),
allowNull: false
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{
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<NoteAttributes, 'id' | 'title'>;
// And with a functional approach defining a module looks like this
const Note: ModelDefined<
NoteAttributes,
NoteCreationAttributes
> = sequelize.define(
'Note',
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
title: {
type: new DataTypes.STRING(64),
defaultValue: 'Unnamed Note'
},
content: {
type: new DataTypes.STRING(4096),
allowNull: false
}
},
{
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`!
});
Address.belongsTo(User, { targetKey: 'id' });
User.hasOne(Address, { sourceKey: 'id' });
async function doStuffWithUser() {
const newUser = await User.create({
name: 'Johnny',
preferredName: 'John',
});
console.log(newUser.id, newUser.name, newUser.preferredName);
const project = await newUser.createProject({
name: 'first!'
});
const ourUser = await User.findByPk(1, {
include: [User.associations.projects],
rejectOnEmpty: true // Specifying true here removes `null` from the return type!
});
// Note the `!` null assertion since TS can't know if we included
// the model or not
console.log(ourUser.projects![0].name);
}
(async () => {
await sequelize.sync();
await doStuffWithUser();
})();
關於 Model.init
的情況
Model.init
需要為類型宣告中宣告的每個屬性提供屬性設定。
有些屬性實際上不需要傳遞給 Model.init
,這就是您可以讓這個靜態方法知道它們的方式
-
用於定義關聯的方法 (
Model.belongsTo
、Model.hasMany
等…) 已經處理了必要的外鍵屬性的設定。不需要使用Model.init
來設定這些外鍵。使用ForeignKey<>
品牌類型來讓Model.init
知道不需要設定外鍵import {
Model,
InferAttributes,
InferCreationAttributes,
DataTypes,
ForeignKey,
} from 'sequelize';
class Project extends Model<InferAttributes<Project>, InferCreationAttributes<Project>> {
id: number;
userId: ForeignKey<number>;
}
// this configures the `userId` attribute.
Project.belongsTo(User);
// therefore, `userId` doesn't need to be specified here.
Project.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
},
{ sequelize },
); -
由 Sequelize 管理的時間戳記屬性 (預設情況下,
createdAt
、updatedAt
和deletedAt
) 不需要使用Model.init
設定,但遺憾的是,Model.init
無法知道這一點。我們建議您使用最少的必要設定來關閉此錯誤import { Model, InferAttributes, InferCreationAttributes, DataTypes } from 'sequelize';
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
id: number;
createdAt: Date;
updatedAt: Date;
}
User.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
// technically, `createdAt` & `updatedAt` are added by Sequelize and don't need to be configured in Model.init
// but the typings of Model.init do not know this. Add the following to mute the typing error:
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{ sequelize },
);
不使用嚴格屬性類型的使用方式
Sequelize v5 的類型宣告允許您定義模型而無需指定屬性的類型。為了向後相容性,以及在您認為屬性的嚴格類型不值得的情況下,這仍然是可能的。
/**
* 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 {
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);
}
使用 Sequelize#define
在 v5 之前的 Sequelize 版本中,定義模型的預設方式是使用 Sequelize#define
。仍然可以使用該方法來定義模型,您也可以使用介面將類型宣告新增至這些模型。
/**
* Keep this file in sync with the code in the "Usage of `sequelize.define`"
* section in /docs/manual/other-topics/typescript.md
*
* Don't include this comment in the md file.
*/
import { Sequelize, Model, DataTypes, CreationOptional, InferAttributes, InferCreationAttributes } 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 UserModel extends Model<InferAttributes<UserModel>, InferCreationAttributes<UserModel>> {
// Some fields are optional when calling UserModel.create() or UserModel.build()
id: CreationOptional<number>;
name: string;
}
const UserModel = sequelize.define<UserModel>('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);
}
實用類型
請求模型類別
ModelStatic
設計用於為模型類別設定類型。
以下是一個實用方法的範例,該方法會請求模型類別,並傳回該類別中定義的主鍵清單
import {
ModelStatic,
ModelAttributeColumnOptions,
Model,
InferAttributes,
InferCreationAttributes,
CreationOptional,
} from 'sequelize';
/**
* Returns the list of attributes that are part of the model's primary key.
*/
export function getPrimaryKeyAttributes(model: ModelStatic<any>): ModelAttributeColumnOptions[] {
const attributes: ModelAttributeColumnOptions[] = [];
for (const attribute of Object.values(model.rawAttributes)) {
if (attribute.primaryKey) {
attributes.push(attribute);
}
}
return attributes;
}
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
id: CreationOptional<number>;
}
User.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
},
{ sequelize },
);
const primaryAttributes = getPrimaryKeyAttributes(User);
取得模型的屬性
如果您需要存取指定模型的屬性清單,則 Attributes<Model>
和 CreationAttributes<Model>
是您需要使用的類型。
它們將傳回作為參數傳遞的模型的屬性 (和建立屬性)。
不要將它們與 InferAttributes
和 InferCreationAttributes
混淆。這兩種實用類型應該只在定義模型時使用,以自動從模型的公共類別欄位建立屬性清單。它們僅適用於基於類別的模型定義 (當使用 Model.init
時)。
Attributes<Model>
和 CreationAttributes<Model>
將傳回任何模型的屬性清單,無論它們是如何建立的 (無論是 Model.init
還是 Sequelize#define
)。
以下是一個實用函數的範例,該函數會請求模型類別和屬性的名稱;並傳回對應的屬性元數據。
import {
ModelStatic,
ModelAttributeColumnOptions,
Model,
InferAttributes,
InferCreationAttributes,
CreationOptional,
Attributes,
} from 'sequelize';
export function getAttributeMetadata<M extends Model>(
model: ModelStatic<M>,
attributeName: keyof Attributes<M>,
): ModelAttributeColumnOptions {
const attribute = model.rawAttributes[attributeName];
if (attribute == null) {
throw new Error(`Attribute ${attributeName} does not exist on model ${model.name}`);
}
return attribute;
}
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
id: CreationOptional<number>;
}
User.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
},
{ sequelize },
);
const idAttributeMeta = getAttributeMetadata(User, 'id'); // works!
// @ts-expect-error
const nameAttributeMeta = getAttributeMetadata(User, 'name'); // fails because 'name' is not an attribute of User