You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cross-package env types still break in monorepos when one package imports another and both have their own .env.schema. The fix in #594 (unique hashed type aliases) only addressed duplicate-identifier collisions when both generated env.d.ts files land in one compilation — it does not (and can't) address the underlying issue from #582. We should revisit the typegen system to offer a per-package ENV type that doesn't rely on global module augmentation.
Reproduction
@_/lib-b imports a function from @_/lib-a; both have their own .env.schema/env.d.ts. Running tsc in lib-b:
../lib-a/src/bar.ts(5,14): error TS2339: Property 'MY_VAR' does not exist on type 'TypedEnvSchema'.
(Standalone repro available; can attach.)
Root cause
varlock/env exports ENV: TypedEnvSchema, where TypedEnvSchema is an empty interface that each package's generated env.d.ts augments via declare module 'varlock/env'. There is exactly onevarlock/env module in the whole TS program, so ENV's type is the merge of every env.d.ts in the current compilation.
When lib-b follows an import into lib-a's source (bar.ts), tsc type-checks that body — including ENV.MY_VAR — but only lib-b's env.d.ts is loaded, so TypedEnvSchema has only lib-b's keys → MY_VAR "does not exist". This is an inherent limitation of global module augmentation; no small typegen tweak fixes it while keeping import { ENV } from 'varlock/env'.
Current workaround (no varlock change)
Consume sibling packages through their built .d.ts, not source — tsc infers and inlines ENV.*-derived types at the build boundary (foobar(): string | undefined), so the augmentation never has to agree across packages. Concretely: types/default export conditions pointing at dist, TS project references + tsc -b, and disableSourceOfProjectReferenceRedirect: true.
Downside: forces build-first ordering and gives up source-first (ts-src) cross-package type-checking — which is exactly the DX our own packages rely on.
// user codeimport{ENV}from'./env';// package-local, not 'varlock/env'
The type now travels with the package by module identity, so it resolves correctly even when lib-a's source is compiled inside lib-b — with no build step and full ts-src source-first dev. Runtime is identical (same underlying proxy; only the type is package-scoped).
Notes / gotchas
Reference the local module via a relative import (./env) or a package self-reference (@_/lib-a/env via an exports entry). A tsconfig path alias (@/env) is resolved against the current compilation's tsconfig and so reproduces the exact bug — must be called out loudly in docs and the generated file.
NodeJS.ProcessEnv / import.meta.env can likely stay globally augmented — union-merge of additive string keys across packages is harmless there. Only the strict typed ENV object needs to be package-local.
Likely an opt-in / hybrid: keep the current global-augmentation env.d.ts as the default (nicer zero-wiring DX for single apps), add a "module mode" for library packages consumed in monorepos. Directly delivers the reporter's suggestions 1 & 3 from [BUG]: declare module 'varlock/env' breaks in Monorepo setups #582.
Scope
Before implementing, we may want to revisit the typegen system overall (how @generateTypes is configured, whether to emit .ts vs .d.ts, how integrations declare their preferred mode) rather than bolt this on. Filing to track.
Summary
Cross-package env types still break in monorepos when one package imports another and both have their own
.env.schema. The fix in #594 (unique hashed type aliases) only addressed duplicate-identifier collisions when both generatedenv.d.tsfiles land in one compilation — it does not (and can't) address the underlying issue from #582. We should revisit the typegen system to offer a per-package ENV type that doesn't rely on global module augmentation.Reproduction
@_/lib-bimports a function from@_/lib-a; both have their own.env.schema/env.d.ts. Runningtscin lib-b:(Standalone repro available; can attach.)
Root cause
varlock/envexportsENV: TypedEnvSchema, whereTypedEnvSchemais an empty interface that each package's generatedenv.d.tsaugments viadeclare module 'varlock/env'. There is exactly onevarlock/envmodule in the whole TS program, soENV's type is the merge of everyenv.d.tsin the current compilation.When lib-b follows an import into lib-a's source (
bar.ts), tsc type-checks that body — includingENV.MY_VAR— but only lib-b'senv.d.tsis loaded, soTypedEnvSchemahas only lib-b's keys →MY_VAR"does not exist". This is an inherent limitation of global module augmentation; no small typegen tweak fixes it while keepingimport { ENV } from 'varlock/env'.Current workaround (no varlock change)
Consume sibling packages through their built
.d.ts, not source —tscinfers and inlinesENV.*-derived types at the build boundary (foobar(): string | undefined), so the augmentation never has to agree across packages. Concretely:types/defaultexport conditions pointing atdist, TS project references +tsc -b, anddisableSourceOfProjectReferenceRedirect: true.Downside: forces build-first ordering and gives up source-first (
ts-src) cross-package type-checking — which is exactly the DX our own packages rely on.Proposed direction: package-local generated ENV module (opt-in)
Have
typegenemit a real per-package module that exports an already-typedENV, instead of augmenting the globalvarlock/env:The type now travels with the package by module identity, so it resolves correctly even when lib-a's source is compiled inside lib-b — with no build step and full
ts-srcsource-first dev. Runtime is identical (same underlying proxy; only the type is package-scoped).Notes / gotchas
./env) or a package self-reference (@_/lib-a/envvia anexportsentry). A tsconfig path alias (@/env) is resolved against the current compilation's tsconfig and so reproduces the exact bug — must be called out loudly in docs and the generated file.NodeJS.ProcessEnv/import.meta.envcan likely stay globally augmented — union-merge of additive string keys across packages is harmless there. Only the strict typedENVobject needs to be package-local.env.d.tsas the default (nicer zero-wiring DX for single apps), add a "module mode" for library packages consumed in monorepos. Directly delivers the reporter's suggestions 1 & 3 from [BUG]:declare module 'varlock/env'breaks in Monorepo setups #582.Scope
Before implementing, we may want to revisit the typegen system overall (how
@generateTypesis configured, whether to emit.tsvs.d.ts, how integrations declare their preferred mode) rather than bolt this on. Filing to track.Related: #582, #594