Problem
Effect keys a lot of runtime identity on plain string literals: Schema.ErrorClass / Schema.Class identifiers, Data.TaggedError tags, Context.Service keys, and *TypeId constants following the path-shaped convention ~<package>/<Module>/<Name>. Since these are just strings, two failure modes are completely invisible to the compiler:
- Copy-paste mismatch — a path-shaped id copied from another module still points at the source package/module (e.g. a
RedisError in package myapp carrying "~@effect/platform-bun/BunRedis/RedisError"). Guards, service-key identity, and serialization compatibility now key on the wrong id, and it can collide with the module it was copied from.
- Program-wide duplicate — two distinct declarations resolving to the identical id string. For
Context.Service this means both classes occupy the same Context slot, so providing one silently satisfies the other.
The Effect team shipped exactly this bug: commit b85c386ba fixed Redis type ids in effect/unstable/persistence/Redis that had been copy-pasted from platform-bun ("~@effect/platform-bun/BunRedis/RedisError" → "~effect/persistence/Redis/RedisError").
Verified against effect@4.0.0-beta.101 with TypeScript 7.0.2 --strict: both variants compile with zero errors, and the runtime harm is confirmed.
Reproduction
// file: src/persistence/Redis.ts in package "myapp"
import { Context, Schema } from "effect"
// (a) TypeId copy-pasted from @effect/platform-bun — path-shaped, wrong package & module
const ErrorTypeId = "~@effect/platform-bun/BunRedis/RedisError"
export class RedisError extends Schema.ErrorClass<RedisError>(ErrorTypeId)({
reason: Schema.String
}) {
readonly [ErrorTypeId] = ErrorTypeId
}
// (b) Two unrelated services sharing one key string — same slot in a Context
export class Database extends Context.Service<Database, {
readonly query: (sql: string) => string
}>()("myapp/Database") {}
export class Analytics extends Context.Service<Analytics, {
readonly track: (event: string) => void
}>()("myapp/Database") {}
const ctx = Context.make(Database, { query: (sql) => `Result: ${sql}` })
const analytics = Context.getOrElse(ctx, Analytics, () => undefined)
console.log(analytics) // Database implementation leaks into the Analytics slot
tsc --strict reports nothing. At runtime, providing only Database makes the unrelated Analytics service resolve to the Database implementation (confirmed with tsx), and RedisError carries an id claiming to live in @effect/platform-bun.
Proposed rule behavior
Collect string literals used as identity arguments — Schema.ErrorClass / Schema.Class identifiers, Data.TaggedError tags, Context.Service keys — and initializers of const *TypeId declarations. Then report:
- Exact program-wide duplicate (always safe): two distinct declarations resolving to an identical id string. Report on the later string literal:
"Identifier '~@effect/platform-bun/BunRedis/RedisError' is already used by 'BunRedisError' in BunRedis.ts. Duplicate identifiers share runtime identity (guards, service keys, serialization)."
- Path-shape mismatch (heuristic, only for ids matching
^~?[\w@./-]+/): the package segment differs from the containing package.json name, or the module segment differs from the containing file basename. Report on the string literal: "TypeId '~@effect/platform-bun/BunRedis/RedisError' does not match this module; expected something like '~myapp/Redis/RedisError'." Code fix: rewrite the id to the expected ~<package>/<file-basename>/<ClassName> form (the same fix commit b85c386ba applied by hand).
Must NOT flag:
- Non-path-shaped ids (plain tags like
"CacheError") for the mismatch check — only the exact-duplicate check applies to those.
- Re-exports or multiple references to the same declaration (only distinct declarations count as duplicates).
- Ids inside
node_modules — third-party ids are out of scope; only in-project declarations are checked.
- Intentional interop where the id string matches once the path-shape heuristic can't confidently resolve package/module (e.g. no enclosing
package.json).
This complements deterministicKeys (which is about key stability, not cross-declaration collisions) and redundantSchemaTagIdentifier (which flags an identifier equal to the tag, i.e. the opposite problem); neither checks id content against the declaration site or across the program, so there is no overlap.
Proposed rule name
mismatchedTypeIdIdentifier — matches the classSelfMismatch naming style and names both the artifact (TypeId/identifier) and the defect (mismatch).
duplicateTypeId — mirrors duplicatePackage, but undersells the path-mismatch half of the rule.
copyPastedTypeId — vivid about the root cause, but less precise about what is actually detected.
Problem
Effect keys a lot of runtime identity on plain string literals:
Schema.ErrorClass/Schema.Classidentifiers,Data.TaggedErrortags,Context.Servicekeys, and*TypeIdconstants following the path-shaped convention~<package>/<Module>/<Name>. Since these are just strings, two failure modes are completely invisible to the compiler:RedisErrorin packagemyappcarrying"~@effect/platform-bun/BunRedis/RedisError"). Guards, service-key identity, and serialization compatibility now key on the wrong id, and it can collide with the module it was copied from.Context.Servicethis means both classes occupy the same Context slot, so providing one silently satisfies the other.The Effect team shipped exactly this bug: commit
b85c386bafixed Redis type ids ineffect/unstable/persistence/Redisthat had been copy-pasted from platform-bun ("~@effect/platform-bun/BunRedis/RedisError"→"~effect/persistence/Redis/RedisError").Verified against
effect@4.0.0-beta.101with TypeScript 7.0.2--strict: both variants compile with zero errors, and the runtime harm is confirmed.Reproduction
tsc --strictreports nothing. At runtime, providing onlyDatabasemakes the unrelatedAnalyticsservice resolve to the Database implementation (confirmed with tsx), andRedisErrorcarries an id claiming to live in@effect/platform-bun.Proposed rule behavior
Collect string literals used as identity arguments —
Schema.ErrorClass/Schema.Classidentifiers,Data.TaggedErrortags,Context.Servicekeys — and initializers ofconst *TypeIddeclarations. Then report:"Identifier '~@effect/platform-bun/BunRedis/RedisError' is already used by 'BunRedisError' in BunRedis.ts. Duplicate identifiers share runtime identity (guards, service keys, serialization)."^~?[\w@./-]+/): the package segment differs from the containingpackage.jsonname, or the module segment differs from the containing file basename. Report on the string literal:"TypeId '~@effect/platform-bun/BunRedis/RedisError' does not match this module; expected something like '~myapp/Redis/RedisError'."Code fix: rewrite the id to the expected~<package>/<file-basename>/<ClassName>form (the same fix commitb85c386baapplied by hand).Must NOT flag:
"CacheError") for the mismatch check — only the exact-duplicate check applies to those.node_modules— third-party ids are out of scope; only in-project declarations are checked.package.json).This complements
deterministicKeys(which is about key stability, not cross-declaration collisions) andredundantSchemaTagIdentifier(which flags an identifier equal to the tag, i.e. the opposite problem); neither checks id content against the declaration site or across the program, so there is no overlap.Proposed rule name
mismatchedTypeIdIdentifier— matches theclassSelfMismatchnaming style and names both the artifact (TypeId/identifier) and the defect (mismatch).duplicateTypeId— mirrorsduplicatePackage, but undersells the path-mismatch half of the rule.copyPastedTypeId— vivid about the root cause, but less precise about what is actually detected.