If you're writing Sequelize models in TypeScript and casually declaring fields with public, you might be silently breaking every single attribute getter. Here's how all 23 models in our project were broken without a single error message.
What Happened
During integration testing, every customer name and supplier name came back as null from the API. The data was clearly in the database — SELECT * FROM sales returned customer_name = 'Test Corp' just fine. But the API response? All null.
The Cause: Class Fields Shadow Sequelize's Getters
Here's the problematic code:
// ❌ This is broken
class Sale extends Model {
public customer_name!: string;
public total_amount!: number;
// ...
}
TypeScript's public class field declarations compile down to constructor-level property assignments: this.customer_name = undefined. This completely overwrites the getter/setter that Sequelize sets up on the prototype.
Under the ES2022 class field specification, class fields use [[Define]] semantics (not Object.defineProperty). This means they bypass the prototype chain entirely and write undefined directly onto the instance, shadowing any getter/setter defined by Sequelize.
The Fix: Use declare
// ✅ This is correct
class Sale extends Model {
declare customer_name: string;
declare total_amount: number;
// ...
}
declare is a TypeScript-only construct that exists purely for type checking — it emits zero JavaScript. Sequelize's getter/setter survives untouched.
One caveat: you cannot combine declare with ! (non-null assertion) — that's TS1255. Since declare already means "this property is initialized elsewhere," the ! is redundant anyway.
Scope of the Fix
All 23 models needed public xxx!: type → declare xxx: type:
- Sale, SaleDetail, Arrival, ArrivalDetail, Inventory
- Customer, Supplier, Warehouse, Unit, Department
- Purchase, PurchaseDetail, Payment, PaymentDetail
- Shipment, ShipmentDetail, Quotation
- AccountsPayable, AccountsReceivable, CompanyAccount, Company
- DeliveryDestination, PaymentDestination, ProcurementAlert
After the fix: npx tsc → zero errors → deploy → all fields returned correctly.
Why It Went Unnoticed
Unit tests typically use the return value of Model.create() directly. Since create() builds a fresh instance and sets values through setters, getter shadowing doesn't bite you.
The problem only surfaces on Model.findAll() or Model.findOne() — when Sequelize tries to read values through getters and hits the shadowed undefined instead.
It took integration testing — "insert data via one endpoint, read via another" — to expose the bug.
Takeaways
-
Always use
declarefor Sequelize + TypeScript model attributes. The official docs say so, but many tutorials and code generators still usepublic. -
Class field shadowing fails silently. No errors, no warnings — just
null/undefinedvalues at runtime. - Integration tests catch what unit tests miss. This is a textbook example.
Top comments (0)