Skip to content

Conversation

@benzaria
Copy link
Contributor

@benzaria benzaria commented Jun 16, 2025

Closes #1190

Changes

  • fix Paths accepts index signature types and output all possible paths.
  • refactor: split type into smaller pices and removed unnecessary conditions and repetition in Paths.
  • refactor: Updated LiteralUnion to return (string & {}) instead of (string & Record<never, never>).
  • add: Introduced EmptyArray and IsEmptyArray types (internal).
  • add: Introduced OwnKeys types that extract own proprety keys from object/arrays.

Tests

  • add: Added tests for new features and bug fixes.
  • update: Updated existing tests to reflect changes.

@benzaria benzaria changed the title Fix: Path returning never for index signature types Fix: Paths returning never for index signature types Jun 16, 2025
@benzaria benzaria marked this pull request as draft June 16, 2025 21:29
 **Changes**
 - fix: `Paths` no longer returns `never` for index signature types.
 - refactor: Removed unnecessary conditions and repetition in `Paths`.
 - refactor: Updated `LiteralUnion` to return `(string & {})` instead of `(string & Record<never, never>)`.
 - add: Introduced `EmptyArray` and `IsEmptyArray` types (internal).
 - fix: `IsEmptyObject` no longer returns `never` for `never`.

 **Tests**
 - add: Added tests for new features and bug fixes.
 - update: Updated existing tests to reflect changes.
@benzaria benzaria marked this pull request as ready for review June 16, 2025 21:45
@sindresorhus
Copy link
Owner

refactor: Updated LiteralUnion to return (string & {}) instead of (string & Record<never, never>).

Can you elaborate on why that is better? And it needs a test, if possible.

@benzaria
Copy link
Contributor Author

refactor: Updated LiteralUnion to return (string & {}) instead of (string & Record<never, never>).

Can you elaborate on why that is better? And it needs a test, if possible.

@sindresorhus Its just a visual improvement that does not change the functionality of the type.

I test it using this example and some others should I add a test file?

type LiteralUnion<LiteralType, BaseType extends Primitive> = LiteralType | (BaseType & Simplify<{}>);

// Pass
expectType<LiteralUnion<'foo', string>>({} as (string & {}) | 'foo');
expectType<LiteralUnion<'foo', string>>({} as (string & Record<never, never>) | 'foo');
// Fail
expectType<string>({} as (string & {}) | 'foo');

Copy link
Collaborator

@som-sm som-sm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could return an uncollapsed union like (string & {}) | `${string}.a` | `${string}.b`, but we’d have to decide in what situations we should return an uncollapsed union.

As mentioned here, just adding a check for string wouldn't be sufficient, because there would be other cases as well where the result would get collapsed.

The existing implementation fails in the following cases:

  •  type T = Paths<{[x: Uppercase<string>]: {a: string}; C: {a: string}}>;
     //=> Uppercase<string> | `${Uppercase<string>}.a`

    C.a is lost, result should have probably been: Uppercase<string> | (`${Uppercase<string>}.a` & {}) | "C.a"

  •  type T = Paths<{a: {[x: string]: number} | {b: number}}>;
     //=> "a" | `a.${string & {}}`

    a.b is lost, result should have probably been 'a' | (`a.${string}` & {}) | 'a.b'.

There would likely be more cases, hence it needs to be thought out properly.


Also, it'd be much easier to review if the PR only fixed the bug and not introduced a bunch of refactoring changes along with it.

Comment on lines 201 to 220
[Key in keyof T]:
Key extends string | number // Limit `Key` to string or number.
? (
Options['bracketNotation'] extends true
? IsNumberLike<Key> extends true
? `[${Key}]`
: (Key | ToString<Key>)
: Options['bracketNotation'] extends false
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
? (Key | ToString<Key>)
: never
) extends infer TranformedKey extends string | number ?
[Key in keyof T]: Key extends PathableKeys // Limit `Key` to string or number.
? (Options['bracketNotation'] extends true
? IsNumberLike<Key> extends true
? `[${Key}]`
: (Key | ToString<Key>)
: (Key | ToString<Key>) // If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
) extends infer TranformedKey extends PathableKeys ?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like an irrelevant change.

/**
Represents pathable keys, the `string` or `number` value.
*/
type PathableKeys = string | number;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, this should not be part of this PR. It introduces a bunch of refactoring changes, which makes it harder to review the actual bug-related changes.

Comment on lines -246 to -255
) | (
Options['bracketNotation'] extends false
? `${TranformedKey}.${SubPath}`
: never
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove Options['bracketNotation'] extends false conditional in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if its not true -> its false there is no in bettwen. bracketNotation extends boolean !

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do that in this PR, what's the scope of this PR?

? IsNumberLike<Key> extends true
? `[${Key}]`
: (Key | ToString<Key>)
: Options['bracketNotation'] extends false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove Options['bracketNotation'] extends false conditional in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the purpose of it in the first place?

Repository owner deleted a comment Jun 17, 2025
benzaria added 4 commits June 18, 2025 00:06
 **Changes**
 - split `Paths` into smaller types.
 - introduce `FilterWideTypes` to catch wide types.
 - add `Key` condtions to manipulate wide types keys.
 - refactor types structure, improving types logic and remove unnessasary condition.
@benzaria
Copy link
Contributor Author

benzaria commented Jun 18, 2025

@sindresorhus @som-sm Hey guys, so I wanted to discuss the new changes.
I was able to add the intended behavior and covering edge cases that @som-sm mentioned:

declare const edge1: Paths<{a: {[x: string]: number} | {b: number}}>;
expectType<'a' | 'a.b' | (`a.${string}` & {})>(edge1); // Pass

declare const edge2: Paths<{[x: Uppercase<string>]: {a: string}; C: {a: string}}>;
expectType<'C' | 'C.a' | (Uppercase<string> & {}) | (`${Uppercase<string>}.a` & {})>(edge2); // Pass

declare const stringRecord: Paths<Record<string, {a: number; b: number}>>;
expectType<(string & {}) | `${string}.a` | `${string}.b`>(stringRecord); // Pass

declare const templateRecord: Paths<Record<`on${string}`, {a: number; b: number}>>;
expectType<(`on${string}` & {}) | (`on${string}.a` & {}) | (`on${string}.b` & {})>(templateRecord); // Pass

declare const complexRecord: Paths<Record<number, {a: number; b: number}> & DeepObject>;
expectType<number | `${number}` | 'a' | (`${number}.b` & {}) | (`${number}.a` & {}) | 'a.b' | 'a.b2' | 'a.b3' | 'a.b.c' | ${`a.b2.${number}` & {}} | 'a.b.c.d'>(complexRecord); // Pass

Explaination
Now Paths will return a LiteralUnion for any wide string type like (string, Uppercase<string>, on${number})
For now test are fine but they will fail bcs string !== (string & {}) so we need to go over test and make some changes.

At the moment this it the best result so far, but maybe I can find a way to include the LiteralUnion only if types extends others but that will be triky to achieve.

Also, it'd be much easier to review if the PR only fixed the bug and not introduced a bunch of refactoring changes along with it.

I do apologize for the extra refactoring and changes, but I couldn't stand the compact type and all-in-one approach that was originally used. A best practice is to split the types into reusable pices that do one job. I apologize again 🙏for the mess.

@benzaria benzaria changed the title Fix: Paths returning never for index signature types Fix: add index signature support for Paths Jun 18, 2025
@sindresorhus
Copy link
Owner

I do apologize for the extra refactoring and changes, but I couldn't stand the compact type and all-in-one approach that was originally used. A best practice is to split the types into reusable pices that do one job.

We are more than happy to have refactoring and readability improvements. What we are trying to tell you is that those changes are better done separately as they make it hard to see the actual changes.

@benzaria
Copy link
Contributor Author

Okay captain, lets keep it for another PR.
@som-sm I reverted the refactoring changes.

@benzaria benzaria requested a review from som-sm June 20, 2025 13:56
@sindresorhus
Copy link
Owner

@sindresorhus
Copy link
Owner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Paths should handle Record types with arbitrary string keys

3 participants