Paths: Fix behavior with generic types#1343
Merged
sindresorhus merged 3 commits intomainfrom Jan 31, 2026
Merged
Conversation
3b76ac9 to
bd57022
Compare
som-sm
commented
Jan 30, 2026
Comment on lines
46
to
+51
| export type IsNumberLike<N> = | ||
| IsAnyOrNever<N> extends true ? N | ||
| : N extends number | `${number}` | ||
| IfNotAnyOrNever<N, | ||
| N extends number | `${number}` | ||
| ? true | ||
| : false; | ||
| : false, | ||
| boolean, false>; |
Collaborator
Author
There was a problem hiding this comment.
IsNumberLike should always return a boolean, but currently it returns any for any, which makes it harder to instantiate at places that have a boolean constraint, like:
And<Options['bracketNotation'], IsNumberLike<Key>>bd57022 to
c4685a7
Compare
Owner
|
I added a couple of more tests just to make it comprehensive (not directly related to this change): 2f93f4b |
sindresorhus
approved these changes
Jan 31, 2026
Owner
sindresorhus
left a comment
There was a problem hiding this comment.
Looks good. Thanks for the comprehensive explanation of the changes.
Collaborator
Author
Cool, looks good! |
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.
Fixes #1340
Reproducing the problem
This is an interesting one.
Let's consider this really simplified implementation of
Paths:Now, let's recreate the problem in #1340 using this simplified implementation:
Everything works fine here.
SimplifiedPathsis able to figure out that'bar.baz'is a valid path forFoo<T>.And, if you intentionally instantiate
SomeTypeWithConstraintwith some incorrect path, then you'd see that the complete value forSimplifiedPaths<Foo<T>>is'bar' | 'bar.baz' | `bar.baz.${SimplifiedPaths<T> & string}`, which is perfect!A small change that breaks it
Now let’s make a small change to
SimplifiedPaths:The only difference is that we first capture
SimplifiedPaths<T[P]>intoSubPathsand then use that.Looks harmless, but this now causes an error in our
SomeTypeWithConstraintinstantiation:If you inspect the error,
SimplifiedPaths<Foo<T>>now looks like this:What changed?
Now, the entire subpath chain is wrapped in conditionals that depend on
SimplifiedPaths<T>. That makes even'bar.baz'indirectly dependent onT.So,
'bar.baz'is no longer a clearly known union member. It is buried inside generic-dependent conditionals, which is why the constraint fails.Why did
Pathsextract the recursive result in the first place?This was done to handle
bracketNotationcorrectly.If a subpath starts with bracket notation, we must not insert a dot before it. So the caller needs to know what the recursive result looks like before deciding whether to join with
.or not. And that is what introduced the generic dependency problem.Refer to the following lines in the existing implementation:
type-fest/source/paths.d.ts
Lines 256 to 268 in 77672ac
The fix
This PR moves dot handling into the recursive step itself.
Instead of the caller deciding whether to add a dot, each recursive subpath now returns paths that are already correctly prefixed.
Because of that:
As a result, TS can simplify known prefixes like
'bar.baz', and the constraint works as expected.Additional improvement
Additionally, this also uncollapses some paths that were previously getting collapsed.
For example, earlier:
returned:
This effectively collapsed
`a.${string}.b`and`a.${string}.c`into just`a.${string}`. But those paths do not actually collapse into`a.${string}`. This was only happening because of a limitation in the previous implementation.The issue was that the subpath being returned for
'a'wasstring | `${string}.b` | `${string}.c`which collapses to juststring. When that gets joined with'a', the final result becomes only'a' | `a.${string}`.So even though the entire result didn't require any collapsing, it happened because the subpath had already collapsed.
With the updated logic, subpaths are prefixed with a dot. In this case, the subpath now becomes:
This union does not collapse. So when joined with
'a', we correctly get:So this PR also removes any unnecessary collapsing.
Note
This does not eliminate collapsing entirely. Something like:
still resolves to just
string, not(string & {}) | 'a'. So #1190 still remains valid.