Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions source/internal/numeric.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {IsNever} from '../is-never.d.ts';
import type {Finite, NegativeInfinity, PositiveInfinity} from '../numeric.d.ts';
import type {UnknownArray} from '../unknown-array.d.ts';
import type {StringToNumber} from './string.d.ts';
import type {IsAnyOrNever} from './type.d.ts';
import type {IfNotAnyOrNever, IsAnyOrNever} from './type.d.ts';

/**
Returns the absolute value of a given value.
Expand Down Expand Up @@ -44,10 +44,11 @@ type E = IsNumberLike<'a'>;
//=> false
*/
export type IsNumberLike<N> =
IsAnyOrNever<N> extends true ? N
: N extends number | `${number}`
IfNotAnyOrNever<N,
N extends number | `${number}`
? true
: false;
: false,
boolean, false>;
Comment on lines 46 to +51
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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>>


/**
Returns the minimum number in the given union of numbers.
Expand Down
55 changes: 17 additions & 38 deletions source/paths.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions} from './internal/index.d.ts';
import type {IsAny} from './is-any.d.ts';
import type {UnknownArray} from './unknown-array.d.ts';
import type {Subtract} from './subtract.d.ts';
import type {GreaterThan} from './greater-than.d.ts';
import type {IsNever} from './is-never.d.ts';
import type {Sum} from './sum.d.ts';
import type {And} from './and.d.ts';

/**
Paths options.
Expand Down Expand Up @@ -190,21 +191,21 @@ open('listB.1'); // TypeError. Because listB only has one element.
*/
export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, Options>>;

type _Paths<T, Options extends Required<PathsOptions>> =
type _Paths<T, Options extends Required<PathsOptions>, CurrentDepth extends number = 0> =
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
? never
: IsAny<T> extends true
? never
: T extends UnknownArray
? number extends T['length']
// We need to handle the fixed and non-fixed index part of the array separately.
? InternalPaths<StaticPartOfArray<T>, Options> | InternalPaths<Array<VariablePartOfArray<T>[number]>, Options>
: InternalPaths<T, Options>
? InternalPaths<StaticPartOfArray<T>, Options, CurrentDepth> | InternalPaths<Array<VariablePartOfArray<T>[number]>, Options, CurrentDepth>
: InternalPaths<T, Options, CurrentDepth>
: T extends object
? InternalPaths<T, Options>
? InternalPaths<T, Options, CurrentDepth>
: never;

type InternalPaths<T, Options extends Required<PathsOptions>> =
type InternalPaths<T, Options extends Required<PathsOptions>, CurrentDepth extends number> =
Options['maxRecursionDepth'] extends infer MaxDepth extends number
? Required<T> extends infer T
? T extends readonly []
Expand All @@ -215,19 +216,17 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
[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
And<Options['bracketNotation'], IsNumberLike<Key>> extends true
? `[${Key}]`
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
? (Key | ToString<Key>)
: never
: CurrentDepth extends 0
? Key | ToString<Key>
: `.${(Key | ToString<Key>)}`
) extends infer TranformedKey extends string | number ?
// 1. If style is 'a[0].b' and 'Key' is a numberlike value like 3 or '3', transform 'Key' to `[${Key}]`, else to `${Key}` | Key
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
| ((Options['leavesOnly'] extends true
? MaxDepth extends 0
? MaxDepth extends CurrentDepth
? TranformedKey
: T[Key] extends infer Value
? (Value extends readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
Expand All @@ -238,36 +237,16 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
: never
: TranformedKey
) extends infer _TransformedKey
// If `depth` is provided, the condition becomes truthy only when it reaches `0`.
// If `depth` is provided, the condition becomes truthy only when it reaches `CurrentDepth`.
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
? 0 extends Options['depth']
? CurrentDepth extends Options['depth']
? _TransformedKey
: never
: never)
| (
// Recursively generate paths for the current key
GreaterThan<MaxDepth, 0> extends true // Limit the depth to prevent infinite recursion
? _Paths<T[Key],
{
bracketNotation: Options['bracketNotation'];
maxRecursionDepth: Subtract<MaxDepth, 1>;
leavesOnly: Options['leavesOnly'];
depth: Subtract<Options['depth'], 1>;
}> extends infer SubPath
? SubPath extends string | number
? (
Options['bracketNotation'] extends true
? SubPath extends `[${any}]` | `[${any}]${string}`
? `${TranformedKey}${SubPath}` // If next node is number key like `[3]`, no need to add `.` before it.
: `${TranformedKey}.${SubPath}`
: never
) | (
Options['bracketNotation'] extends false
? `${TranformedKey}.${SubPath}`
: never
)
: never
: never
GreaterThan<MaxDepth, CurrentDepth> extends true // Limit the depth to prevent infinite recursion
? `${TranformedKey}${_Paths<T[Key], Options, Sum<CurrentDepth, 1>> & (string | number)}`
: never
)
: never
Expand Down
4 changes: 2 additions & 2 deletions test-d/internal/is-number-like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ expectType<IsNumberLike<'5+1.2'>>(false);
expectType<IsNumberLike<'5e-3.1'>>(false);

// Edge cases
expectType<IsNumberLike<never>>({} as never);
expectType<IsNumberLike<any>>({} as any);
expectType<IsNumberLike<never>>(false);
expectType<IsNumberLike<any>>({} as boolean);
expectType<IsNumberLike<number>>(true);
expectType<IsNumberLike<PositiveInfinity>>(true);
expectType<IsNumberLike<NegativeInfinity>>(true);
30 changes: 28 additions & 2 deletions test-d/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ type Object3 = {
};
expectType<Paths<Object3, {bracketNotation: true}>>({} as '[1]' | '[2]');

type Object4 = {
1: {
a: string;
};
};
expectType<Paths<Object4, {bracketNotation: true}>>({} as '[1]' | '[1].a');

type Object5 = {
1: {
2: string;
};
};
expectType<Paths<Object5, {bracketNotation: true}>>({} as '[1]' | '[1][2]');

type deepArray = {
arr: Array<Array<Array<{a: string}>>>;
};
Expand Down Expand Up @@ -417,7 +431,7 @@ declare const indexSignatureWithStaticKeys1: Paths<{[x: Uppercase<string>]: {a:
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys1); // Collapsed union

declare const nonRootIndexSignature: Paths<{a: {[x: string]: {b: string; c: number}}}>;
expectType<'a' | `a.${string}`>(nonRootIndexSignature); // Collapsed union
expectType<'a' | `a.${string}` | `a.${string}.b` | `a.${string}.c`>(nonRootIndexSignature);

declare const nonRootIndexSignature1: Paths<{a: {[x: Lowercase<string>]: {b: string; c: number}}}>;
expectType<'a' | `a.${Lowercase<string>}` | `a.${Lowercase<string>}.b` | `a.${Lowercase<string>}.c`>(nonRootIndexSignature1);
Expand All @@ -443,7 +457,7 @@ declare const indexSignatureLeaves1: Paths<{a: {[x: string]: {b: string; c: numb
expectType<`a.${string}.b` | `a.${string}.c` | 'd' | 'e.f'>(indexSignatureLeaves1);

declare const indexSignatureLeaves2: Paths<{a: {[x: string]: [] | {b: number}}}, {leavesOnly: true}>;
expectType<`a.${string}`>(indexSignatureLeaves2); // Collapsed union
expectType<`a.${string}` | `a.${string}.b`>(indexSignatureLeaves2);

declare const indexSignatureDepth: Paths<{[x: string]: {a: string; b: number}}, {depth: 1}>;
expectType<`${string}.b` | `${string}.a`>(indexSignatureDepth);
Expand All @@ -462,3 +476,15 @@ expectType<`a.${string}.b`>(indexSignatureDepth4);

declare const indexSignatureDepthLeaves: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2; leavesOnly: true}>;
expectType<`a.${string}.b` | `a.${string}.c` | 'd'>(indexSignatureDepthLeaves);

// Generic types
type SomeTypeWithConstraint<T, _U extends Paths<T>> = never;

type Foo<T> = {bar: {baz: T}};
type Test1<T> = SomeTypeWithConstraint<Foo<T>, 'bar.baz'>;

type Bar<T, U> = {bar: {baz: {qux: T}; fizz: {buzz: U} | U | T}};
type Test2<T, U> = SomeTypeWithConstraint<
Bar<T, U>,
'bar' | 'bar.baz' | 'bar.baz.qux' | 'bar.fizz' | 'bar.fizz.buzz'
>;