Problem
Detects Effect Hash values being used as if they were unique identifiers: a Hash.hash(...) result stored as a Map/Record/cache key, or two Hash.hash(...) results compared with ===/!== as a substitute for equality. In Effect, a hash is only a bucketing hint that accelerates Equal.equals; it is deterministic but explicitly not guaranteed unique, so distinct payloads can share a hash.
Why the compiler is silent / what breaks at runtime: Two different payloads can produce the same hash, so the cache hands back data belonging to a different query — exactly the 'wrong data back' bug reported live on stream; it type-checks fine because the hash is just a number.
Both examples below type-check with zero errors against effect@4.0.0-beta.104 (re-verified against that version) 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: hash-as-unique-key
// BAD: Hash.hash(...) is a bucketing hint, not a unique identity. It is
// deterministic but explicitly NOT collision-free: two different payloads can
// hash to the same number. Using the hash as a Map/Record/cache key, or
// comparing two hashes with ===/!== as a stand-in for equality, silently
// conflates distinct values — the cache hands back another query's result.
// It type-checks fine because a hash is just a number.
import { Hash } from "effect"
interface QueryPayload {
readonly table: string
readonly filter: string
readonly page: number
}
interface Result {
readonly rows: ReadonlyArray<string>
}
// The hash number is used as the cache identity: any other payload that
// happens to collide will read/overwrite this entry.
const cache = new Map<number, Result>()
export const remember = (payload: QueryPayload, result: Result): void => {
const key = Hash.hash(payload) // flagged: hash stored as unique key
cache.set(key, result)
}
export const lookup = (payload: QueryPayload): Result | undefined =>
cache.get(Hash.hash(payload)) // flagged: hash used for lookup identity
// Record keyed by hash — same problem, different container.
const byHash: Record<number, Result> = {}
export const rememberRecord = (payload: QueryPayload, result: Result): void => {
byHash[Hash.hash(payload)] = result // flagged
}
// Comparing raw hashes as a substitute for equality: equal hashes do NOT
// imply equal payloads, so this "same query" check is wrong on collisions.
export const isSameQuery = (a: QueryPayload, b: QueryPayload): boolean =>
Hash.hash(a) === Hash.hash(b) // flagged: hash equality treated as identity
export const isDifferentQuery = (a: QueryPayload, b: QueryPayload): boolean =>
Hash.hash(a) !== Hash.hash(b) // flagged (this direction is actually sound
// only for inequality, but the rule flags the identity-comparison pattern)
Good
// RULE: hash-as-unique-key
// GOOD: key caches by the value itself in an Equal-aware structure (HashMap
// uses Hash.hash internally only as a bucket hint and falls back to
// Equal.equals on collision), and compare values with Equal.equals — never by
// comparing raw Hash.hash numbers.
import { Equal, HashMap, Option } from "effect"
interface QueryPayload {
readonly table: string
readonly filter: string
readonly page: number
}
interface Result {
readonly rows: ReadonlyArray<string>
}
// HashMap keys are compared structurally with Equal.equals, so a hash
// collision between two distinct payloads can never mix up entries.
let cache = HashMap.empty<QueryPayload, Result>()
export const remember = (payload: QueryPayload, result: Result): void => {
cache = HashMap.set(cache, payload, result)
}
export const lookup = (payload: QueryPayload): Result | undefined =>
Option.getOrUndefined(HashMap.get(cache, payload))
// Equality between payloads goes through Equal.equals (deep structural
// equality in Effect), which uses the hash only as a fast-path rejection.
export const isSameQuery = (a: QueryPayload, b: QueryPayload): boolean =>
Equal.equals(a, b)
export const isDifferentQuery = (a: QueryPayload, b: QueryPayload): boolean =>
!Equal.equals(a, b)
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
hashAsUniqueKey
Problem
Detects Effect Hash values being used as if they were unique identifiers: a Hash.hash(...) result stored as a Map/Record/cache key, or two Hash.hash(...) results compared with ===/!== as a substitute for equality. In Effect, a hash is only a bucketing hint that accelerates Equal.equals; it is deterministic but explicitly not guaranteed unique, so distinct payloads can share a hash.
Why the compiler is silent / what breaks at runtime: Two different payloads can produce the same hash, so the cache hands back data belonging to a different query — exactly the 'wrong data back' bug reported live on stream; it type-checks fine because the hash is just a number.
Both examples below type-check with zero errors against
effect@4.0.0-beta.104(re-verified against that version) 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
hashAsUniqueKey