fix(types): keyPrefix no longer pollutes t() return type with sibling keys (#2434 regression)#2436
Merged
adrai merged 1 commit intoJun 3, 2026
Conversation
… keys The `[Res] extends [never]` guards added to KeysBuilderWith(out)ReturnObjects in i18next#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>
adrai
added a commit
that referenced
this pull request
Jun 3, 2026
Member
|
Merged and shipped in v26.3.1. Thanks @aaronrosenthal — the write-up was excellent, especially the bit on why the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(types): keyPrefix no longer pollutes t() return type with sibling keys (#2434 regression)
Summary
26.3.0 (#2434) regressed
t()return types for any project that scopes atfunction with a
keyPrefixconstrained toKeyPrefix<Ns>— most notably everyreact-i18nextuseTranslation(ns, { keyPrefix })caller. A leaf lookup nowresolves to a union with unrelated sibling keys' values, and large/deep
resource trees additionally hit
TS2589("Type instantiation is excessivelydeep").
Reproduces under both
tscandtsgo. It does not surface throughgetFixedT(which takeskeyPrefixas a positional argument) or through a rawTFunction<Ns, 'literal'>type argument — only whenKPrefixis inferredagainst the
KeyPrefix<Ns>constraint from an options-object property, which isexactly the
react-i18nextuseTranslationshape.Root cause
#2434 added
[Res] extends [never] ? never : …guards toKeysBuilderWithReturnObjectsandKeysBuilderWithoutReturnObjectsintypescript/t.d.ts.Those guards turn the builders into deferred conditional types. Because
KeyPrefix<Ns> = ResourceKeys<true>[$FirstNamespace<Ns>]flows through thesebuilders,
KeyPrefix<Ns>stops resolving to an eager union of string-literalkey paths. When
KPrefix extends KeyPrefix<Ns>is then inferred from a literalkeyPrefix, TypeScript can no longer match the literal against the deferredconstraint and falls back to the entire
KeyPrefix<Ns>constraint.AppendKeyPrefix<'title', <whole union>>then expands to every<prefix>.title, sot('title')unions all siblingtitlevalues (and theover-expansion blows the instantiation-depth budget on big trees).
Fix
Restore the two builders to their 26.2.0 (eager) form.
I checked whether the guards could be kept in a non-deferring shape and they
can't: the same
[Res] extends [never]deferral re-breaksKeyPrefix<Ns>whether it sits in
KeysBuilderor directly on the concrete childRes[Key].And
nevercan't be detected without the[T] extends [never]trick, sincenever extends Xis alwaystrue.The guards also turn out to be inert for the case they targeted:
shared_conflict) are already resolved by_DropConflictKeysinoptions.d.tsbefore the builders run — a merged namespace is nevernever(worst case
{}).'A' & 'B'→neverleaf) behave identicallywith and without the guard: same
KeyPrefix<Ns>, no garbage keys, theconflicted sub-tree is equally poisoned either way. The guard doesn't rescue
it.
So removing the guards changes no working scenario. If belt-and-suspenders
never-safety is ever wanted, it belongs in the merge layer (options.d.ts),not on the
ResourceKeyspath — a comment int.d.tsrecords this so the guardisn't reintroduced.
Testing
test/typescript/keyprefix-return-type/reproducing thesibling-pollution case via a minimal
useTranslation-shaped helper. Red on theguarded code, green with the fix.
npm run test:typescriptsuite passes (incl.registry-monorepo/registry-monorepo-legacy-only).Fixes the regression introduced in #2434.