Problem
Detects properties in the transformer object passed to Struct.evolve (data-first or pipe form) whose names are not keys of the struct being evolved. Because Evolver is an all-optional mapped type behind a generic E extends Evolver, extra keys pass type-checking, yet the implementation iterates only the target's own keys, so the stray transform never runs. Re-verified against effect 4.0.0-beta.104: both forms compile cleanly with a bogus key. This bites hardest after a field rename, when old evolve call sites keep the stale key and become dead no-ops.
Why the compiler is silent / what breaks at runtime: The evolve call compiles but returns the struct with the intended field untouched — the update quietly stops happening after a rename or an AI-driven refactor leaves garbage keys behind, with no compiler or runtime signal.
Both examples below type-check with zero errors against effect@4.0.0-beta.104 (re-verified) under the monorepo's strict tsconfig, verified with an isolated per-proposal tsconfig — so the compiler offers no protection here and a diagnostic is the only static safety net.
Bad — compiles cleanly, the rule should flag this
// RULE: struct-evolve-extra-keys-silently-ignored
// BAD: the transformer passed to Struct.evolve contains a key ("zipCode")
// that is not a key of the struct being evolved. Evolver<S> is an
// all-optional mapped type behind a generic `E extends Evolver<S>`, so once
// the transformer is not a fresh inline literal (extracted const, reusable
// updater) and shares at least one real key, neither excess-property nor
// weak-type checking fires — yet the implementation iterates only the
// target's own keys, so the stray transform never runs. Classic aftermath
// of a field rename (zipCode -> zip): the extracted updater keeps compiling
// while half of it became a silent no-op.
import { pipe, Struct } from "effect"
const model = {
name: "alice",
zip: "00000" // field was renamed from `zipCode` to `zip`
}
// updater extracted before the rename; the stale `zipCode` key remains
const normalize = {
name: (s: string) => s.toUpperCase(),
zipCode: (s: string) => s.padStart(5, "0")
}
// pipe form: compiles, `name` runs, `zipCode` is silently dropped
const updatedPipe = pipe(model, Struct.evolve(normalize))
void updatedPipe // { name: "ALICE", zip: "00000" } — zip untouched
// data-first form: same silent partial no-op
const updatedDataFirst = Struct.evolve(model, normalize)
void updatedDataFirst
Good
// RULE: struct-evolve-extra-keys-silently-ignored
// GOOD: every key of the transformer object is an actual key of the struct
// being evolved, so each transform really runs. After the zipCode -> zip
// rename, the call site was updated to use the new key name.
import { pipe, Struct } from "effect"
const model = {
name: "alice",
zip: "00000"
}
// pipe form: `zip` is a key of typeof model, transform is applied
const updatedPipe = pipe(
model,
Struct.evolve({ zip: () => "12345" })
)
void updatedPipe // { name: "alice", zip: "12345" }
// data-first form
const updatedDataFirst = Struct.evolve(model, { zip: (z) => z.padStart(5, "0") })
void updatedDataFirst
Where this came up
Mined from the Effect Office Hours playlist; deduplicated against all implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
structEvolveExtraKeysSilentlyIgnored
Problem
Detects properties in the transformer object passed to Struct.evolve (data-first or pipe form) whose names are not keys of the struct being evolved. Because Evolver
is an all-optional mapped type behind a generic E extends Evolver, extra keys pass type-checking, yet the implementation iterates only the target's own keys, so the stray transform never runs. Re-verified against effect 4.0.0-beta.104: both forms compile cleanly with a bogus key. This bites hardest after a field rename, when old evolve call sites keep the stale key and become dead no-ops.Why the compiler is silent / what breaks at runtime: The evolve call compiles but returns the struct with the intended field untouched — the update quietly stops happening after a rename or an AI-driven refactor leaves garbage keys behind, with no compiler or runtime signal.
Both examples below type-check with zero errors against
effect@4.0.0-beta.104(re-verified) under the monorepo's strict tsconfig, verified with an isolated per-proposal tsconfig — so the compiler offers no protection here and a diagnostic is the only static safety net.Bad — compiles cleanly, the rule should flag this
Good
Where this came up
Mined from the Effect Office Hours playlist; deduplicated against all implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
structEvolveExtraKeysSilentlyIgnored