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;
// ...
}
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;
// ...
}
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
-
Always use
declarewith Sequelize + TypeScript. The official docs say so, but plenty of older tutorials and code generators still usepublic -
Class field shadowing fails silently. No errors, no warnings — values just become
null/undefined - Integration tests catch what unit tests can't. This is a textbook example
Top comments (0)