fix(templates): stop class fields shadowing Sequelize's attribute accessors - #48
Merged
Conversation
…essors
Sequelize installs attribute getters and setters on the prototype. `public id!:
string` is emitted as an own property initialised to undefined, which shadows
them: `user.id` reads undefined while `user.get("id")` returns the row's value.
Nothing catches it. The types say `string`, so it compiles, and it surfaces only
at runtime as a query built with an undefined parameter — in practice as
`WHERE parameter "user_id" has invalid "undefined" value` the first time a
signed-in person asks for their own records. Sequelize warns about it at model
init, into a log nobody reads during a scaffold.
`declare` emits no field, so the accessors survive. The caveat is written next to
the model, since this is a pattern anyone adding a model will otherwise copy.
…essors
Sequelize installs attribute getters and setters on the prototype. `public id!:
string` is emitted as an own property initialised to undefined, which shadows
them: `user.id` reads undefined while `user.get("id")` returns the row's value.
Nothing catches it. The types say `string`, so it compiles, and it surfaces only
at runtime as a query built with an undefined parameter, in practice as
`WHERE parameter "user_id" has invalid "undefined" value` the first time a
signed-in person asks for their own records. Sequelize warns about it at model
init, into a log nobody reads during a scaffold.
Whether the field is emitted at all depends on `useDefineForClassFields`, which
follows `target`. Both starters compile at ES2020, where the field is erased and
nothing is shadowed, so this guards the pattern rather than repairing behaviour
anyone is seeing today. The guard is the point: at ES2022 the same code returns
undefined for every attribute, and a `target` bump is an ordinary thing to do.
`declare` emits no field, so the accessors survive.
Bccorb
force-pushed
the
fix/sequelize-class-fields
branch
from
August 20, 2026 02:01
65b720a to
7b278e5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sequelize installs its attribute getters and setters on the prototype.
public id!: stringis emitted as an own property initialised toundefined, which shadows them:user.idreadsundefinedwhileuser.get("id")returns the row's value.declareemits no field at all, so the accessors survive.Both API starters are changed,
expressandfastify. The diff is five lines per model and nothing else.This is a guard, not a repair
Whether the field is emitted depends on
useDefineForClassFields, which followstarget. Both starters compile attarget: ES2020intsconfig.json, andtsconfig.build.jsononly extends it, so dev (tsx) and prod (tsc) agree. Below ES2022 a field with a definite assignment assertion and no initialiser is erased entirely, so nothing is shadowed today.Confirmed by building this branch:
And confirmed in both directions with a live model, no database needed:
So this does not fix behaviour anyone is seeing right now. It matters because it disarms a
targetbump: at ES2022 every attribute on every model returnsundefined, and the first symptom is a query built with an undefined parameter on any handler that filters byreq.appUser.id, whichrequireUserassigns from a Sequelize instance. Moving to a Node 24 baseline is an ordinary thing to do and would trigger it silently.If you have actually observed
WHERE parameter "user_id" has invalid "undefined" value, this branch will not make it go away and the cause is elsewhere.Follow-up worth considering
Nothing in the repo now states the rule, so the pattern is carried by example only. The generation pipeline reads
api/models/user.tsas its worked model example on every run, andreference.tshas a "Conventions to match" block where models are already described. A one line rule there ("attributes are alwaysdeclare, neverpublic x!: T") would make new generated models safe by rule rather than by imitation.Checks
npm run validatepasses.npm run check(typecheck, lint, format:check, test) andnpm run buildpass in both API starters.Note for review
This branch carries two commits because
git commit --amendwas unavailable when the first fixup was prepared. The earlier commit's message contains an em dash, which the working standards forbid. Squash on merge and both go away.