feat(types): add ResourceNamespaceMap and enhance CustomTypeOptions#2434
Conversation
…or monorepo support - Introduced ResourceNamespaceMap interface for per package namespace types. - Updated CustomTypeOptions to work alongside ResourceNamespaceMap, allowing for better type management in monorepos. - Implemented tests to validate the new types and ensure compatibility with existing functionality.
adrai
left a comment
There was a problem hiding this comment.
Thanks for picking this up — checked it out locally and walked the type plumbing end-to-end. Full type-test suite passes (480/480, zero type errors), and the new tests cover the multi-package monorepo scenario exactly as discussed in #2409. The two-step $MergeBy is the right shape; without the second overlay, registry contributions would get dropped whenever CustomTypeOptions.resources is also set.
A few suggestions before merging:
1. Same-key conflict becomes silently never
When both legacy and registry contribute the same namespace and declare the same key with different literals, the & intersection collapses that key to never — no TS error, no warning. The test turns clashing literals on the same key into never only exercises TS's built-in intersection behavior on synthetic types; it doesn't actually go through _MergedResources. Could you add an end-to-end test that triggers this through the real path? Something like adding shared_conflict: 'A' to package-app-legacy.d.ts and shared_conflict: 'B' to package-overlap.d.ts, then asserting t('shared_conflict') returns never on TFunction<'overlap'>.
Not blocking — the behavior itself is the unavoidable trade-off of allowing both surfaces — but if we ship it, we should verify the integration path actually does what we think it does.
2. Expand the HACK: comment
The current comment is honest but a future reader has to reverse-engineer the reasoning. Suggestion:
// Apply merged resources *after* CustomTypeOptions so registry contributions
// survive when CustomTypeOptions.resources is also defined. Without this
// second step, the inner $MergeBy would replace the default `resources: object`
// with CustomTypeOptions['resources'] alone, dropping the registry namespaces.3. Clarify the JSDoc on ResourceNamespaceMap
Worth one extra line spelling out the separation of concerns, so users don't try to put scalar options here:
/**
* ...
* Scalar type options like `defaultNS`, `returnNull`, `enableSelector`,
* etc., still belong on `CustomTypeOptions` — this interface is for
* namespace resource types only.
* ...
*/4. Optional: a selector-mode test for the registry path
All existing selector tests use the legacy CustomTypeOptions.resources. Adding a single selector test (e.g. with enableSelector: 'optimize') using a registry-contributed namespace would prove the selector codegen path also reads through _MergedResources correctly. The internal alias chain says it does — but a test would make the guarantee explicit. Not a blocker; happy to do it as a follow-up.
Aside from those, the change is contained, backwards-compatible, and well-scoped. Verified that no companion changes are needed in react-i18next or next-i18next — neither references TypeOptions['resources'] directly. CHANGELOG can come with the release.
Thanks again for the clean, well-tested PR!
|
@adrai I tried the exact setup (shared_conflict: 'A' in legacy and 'B' in registry, then t('shared_conflict') on TFunction<'overlap'>). |
|
@sh3xu great catch — I reproduced your finding locally, dug into why it happens, and have a verified fix below. You weren't imagining it: the conflict scenario does break the whole Root cause — two TS quirks compoundQuirk 1: type A = { x: 'A'; y: 'common' };
type B = { x: 'B'; z: 'beta' };
type AB = A & B;
// ^? never (NOT { x: never; y: 'common'; z: 'beta' })This means a single same-key/different-literal disagreement ( Quirk 2: Once a resource value becomes Fix (two parts, both required)1.
|
- Added shared_conflict properties to legacy and registry namespaces to manage conflicting literals. - Introduced package-selector.d.ts to define enableSelector in the i18next module. - Enhanced type merging logic to drop conflicting keys while preserving valid ones.
- Added shared_conflict properties to legacy and registry namespaces to manage conflicting literals. - Introduced package-selector.d.ts to define enableSelector in the i18next module. - Enhanced type merging logic to drop conflicting keys while preserving valid ones.
…ub.com/sh3xu/i18next into feat/types-resource-namespace-map-2409
|
Just noticed you pushed updates — thanks for the quick turnaround. Verified locally:
Full TS suite: 496 tests pass, zero type errors, no regressions. Looks good from my side. Are you happy with where it is, or is there anything else you'd like to try before we merge? |
|
@adrai Happy with it as is from my side thanks again for validating everything locally. Nothing else I'd push for before merge. If theres any edge case or additional coverage you think would be valuable here though, I’d be interested to hear your take always useful stuff to learn from. |
|
thx a lot... it's included in v26.3.0 |
… keys (#2436) The `[Res] extends [never]` guards added to KeysBuilderWith(out)ReturnObjects in #2434 defer those builders into conditional types, so `KeyPrefix<Ns>` stops resolving to a literal union. A `KPrefix extends KeyPrefix<Ns>` generic (e.g. react-i18next's useTranslation) then infers the whole constraint instead of the passed literal, polluting t() return types with unrelated sibling keys' values (and TS2589 on deep trees). Restore the eager 26.2.0 builders; the guards were inert (top-level conflicts are handled by _DropConflictKeys before the builders run, and nested conflicts behave identically with or without them). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add
ResourceNamespaceMapto let monorepo packages declare their own i18n namespaces without TS2717. It merges with legacyCustomTypeOptions.resourcesto keep existing augmentations working (closes #2409).ResourceNamespaceMapfor per‑package namespace types in separatei18next.d.tsfiles.TypeOptions['resources']now builds from legacy resources andResourceNamespaceMap, allowing both patterns to coexist.