In the age of Prisma and Drizzle, Sequelize still holds its ground as a mature, battle-tested ORM with excellent support for raw SQL, transactional workflows, and complex associations. But if you’ve ever tried to use Sequelize with TypeScript, you’ve probably bumped into the usual pain points: model typing, messy associations, or that dreaded return of any
.
This guide is for developers who want to use Sequelize in a TypeScript project the right way — with fully typed models, associations, queries, and clean project structure.
🛠️ Project Setup
Let’s start fresh:
1. Initialize your project
mkdir sequelize-ts-guide && cd sequelize-ts-guide
npm init -y
npm install sequelize sequelize-typescript pg reflect-metadata
npm install -D typescript ts-node @types/node
2. TypeScript config (tsconfig.json
)
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
3. Sequelize initialization (src/db.ts
)
import { Sequelize } from 'sequelize-typescript';
import { User } from './models/User';
import { Post } from './models/Post';
export const sequelize = new Sequelize({
dialect: 'postgres',
database: 'sequelize_ts_demo',
username: 'postgres',
password: 'password',
models: [User, Post],
logging: false,
});
📦 Defining Models the Right Way
Use sequelize-typescript
decorators for clean typing and class-based models.
Example: User
model
// src/models/User.ts
import { Table, Column, Model, HasMany } from 'sequelize-typescript';
import { Post } from './Post';
@Table
export class User extends Model<User> {
@Column
name!: string;
@Column
email!: string;
@HasMany(() => Post)
posts!: Post[];
}
Example: Post
model
// src/models/Post.ts
import { Table, Column, Model, ForeignKey, BelongsTo } from 'sequelize-typescript';
import { User } from './User';
@Table
export class Post extends Model<Post> {
@Column
title!: string;
@ForeignKey(() => User)
@Column
userId!: number;
@BelongsTo(() => User)
user!: User;
}
🧠 Typing Models with Generics
Sequelize models can be typed using Model
.
But using sequelize-typescript
, that’s already handled for you. You get autocomplete and strict typings for both creation and querying.
🔍 Type-Safe Queries
const user = await User.findOne({
where: { email: '[email protected]' },
include: [User.associations.posts],
});
if (user) {
user.posts.forEach(post => {
console.log(post.title); // Fully typed!
});
}
Avoid using raw SQL without typing the response. Sequelize will return any
otherwise.
🔗 Associations Done Right
All you need is decorators and proper model imports. No need to define association functions manually unless you want advanced behavior.
hasMany
+ belongsTo
// Already defined above. Access with `user.posts` or `post.user`
🧱 Project Structure
src/
├── db.ts
├── models/
│ ├── User.ts
│ └── Post.ts
├── services/
│ └── userService.ts
└── index.ts
Use services to abstract DB logic from routes/controllers.
Example service:
export const findUserWithPosts = async (id: number) => {
return await User.findByPk(id, {
include: [User.associations.posts],
});
};
✅ Best Practices
- Use
sequelize-typescript
for decorator-based, strongly-typed models - Avoid
any
by never using raw query results without parsing - Use Zod (or Yup) for input validation before hitting Sequelize
- Write repository-style services for clean logic separation
🧨 Common Gotchas
- Circular imports in model associations → use lazy imports in decorators (
() => Model
) - Forgetting
emitDecoratorMetadata
orexperimentalDecorators
→ TS won’t infer types correctly - Not using
sequelize.sync()
or migrations properly → model mismatch with DB
🧭 What’s Next?
Now that you’ve got a fully typed Sequelize setup, the next step is understanding how to type complex associations — especially many-to-many relationships.
In next week’s post, we’ll deep dive into:
“Typed Sequelize Associations in TypeScript — A Deep Dive”
👋 Final Thoughts
Sequelize isn’t going anywhere. It’s used in enterprise apps where flexibility, raw SQL access, and deep control over migrations matter.
But to make it shine in a modern TypeScript stack, you need a clean approach — and now you’ve got one.
Happy querying! 🚀