DEV Community

linou518
linou518

Posted on

Sequelize + TypeScript: Why public Class Fields Silently Break Your Models

Sequelize + TypeScript: Why public Class Fields Silently Break Your Models

If you're writing Sequelize models in TypeScript and declaring fields with public, you might have a ticking time bomb. Here's how all 23 of our models broke without a single error message.

What Happened

During integration testing, every customer name and supplier name came back as null from the API. The database had the data — SELECT * FROM sales showed customer_name = 'Test Corp' plain as day. But the API response? All null.

The Culprit: Class Fields Shadow Sequelize Getters

Here's the offending code:

// ❌ This is the problem
class Sale extends Model {
  public customer_name!: string;
  public total_amount!: number;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

TypeScript's public class fields compile down to constructor assignments: this.customer_name = undefined. This completely overwrites the getter/setter that Sequelize sets up on the prototype.

Under ES2022 class field semantics, instance properties are set using [[Define]] rather than Object.defineProperty. This means prototype chain getters/setters are bypassed entirely — undefined is written directly to the instance.

The Fix: Use declare

// ✅ This is correct
class Sale extends Model {
  declare customer_name: string;
  declare total_amount: number;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

declare exists purely for TypeScript's type checker and emits zero JavaScript. Sequelize's getters and setters remain intact.

One gotcha: you can't combine declare with ! (non-null assertion) — that's a TS1255 error. Since declare already means "this property is initialized elsewhere," the ! is redundant anyway.

Impact and Fix

We replaced public xxx!: type with declare xxx: type across all 23 models — Sale, Customer, Supplier, Inventory, Purchase, Payment, and every other business model.

npx tsc build → zero errors → deploy → all fields returning correctly.

Why We Didn't Catch It Earlier

Unit tests typically use the return value from Model.create(), which creates a new instance and sets values via setters. Getter shadowing doesn't affect this path.

The problem only surfaces with Model.findAll() or Model.findOne() — when Sequelize reads from the database and tries to access values through its getters, it hits the shadowed undefined instead.

It took integration tests — the first time we ran a full "write data → read it back via a different API" flow — to expose the bug.

Lessons Learned

  1. Always use declare with Sequelize + TypeScript. The official docs say so, but plenty of older tutorials and code generators still use public
  2. Class field shadowing fails silently. No errors, no warnings — values just become null/undefined
  3. Integration tests catch what unit tests can't. This is a textbook example

Top comments (0)