Skip to content

Commit 996171b

Browse files
authoredNov 7, 2023
Add Paths type (#741)
1 parent 30aa0ad commit 996171b

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed
 

‎index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export type {IfUnknown} from './source/if-unknown';
103103
export type {ArrayIndices} from './source/array-indices';
104104
export type {ArrayValues} from './source/array-values';
105105
export type {SetFieldType} from './source/set-field-type';
106+
export type {Paths} from './source/paths';
106107

107108
// Template literal types
108109
export type {CamelCase} from './source/camel-case';

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ Click the type names for complete docs.
177177
- [`ArrayIndices`](source/array-indices.d.ts) - Provides valid indices for a constant array or tuple.
178178
- [`ArrayValues`](source/array-values.d.ts) - Provides all values for a constant array or tuple.
179179
- [`SetFieldType`](source/set-field-type.d.ts) - Create a type that changes the type of the given keys.
180+
- [`Paths`](source/paths.d.ts) - Generate a union of all possible paths to properties in the given object.
180181

181182
### Type Guard
182183

‎source/paths.d.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type {EmptyObject} from './empty-object';
2+
import type {IsAny} from './is-any';
3+
import type {IsNever} from './is-never';
4+
import type {UnknownArray} from './unknown-array';
5+
import type {UnknownRecord} from './unknown-record';
6+
7+
type ToString<T> = T extends string | number ? `${T}` : never;
8+
9+
/**
10+
Return the part of the given array with a fixed index.
11+
12+
@example
13+
```
14+
type A = [string, number, boolean, ...string[]];
15+
type B = FilterFixedIndexArray<A>;
16+
//=> [string, number, boolean]
17+
```
18+
*/
19+
type FilterFixedIndexArray<T extends UnknownArray, Result extends UnknownArray = []> =
20+
number extends T['length'] ?
21+
T extends readonly [infer U, ...infer V]
22+
? FilterFixedIndexArray<V, [...Result, U]>
23+
: Result
24+
: T;
25+
26+
/**
27+
Return the part of the given array with a non-fixed index.
28+
29+
@example
30+
```
31+
type A = [string, number, boolean, ...string[]];
32+
type B = FilterNotFixedIndexArray<A>;
33+
//=> string[]
34+
```
35+
*/
36+
type FilterNotFixedIndexArray<T extends UnknownArray> =
37+
T extends readonly [...FilterFixedIndexArray<T>, ...infer U]
38+
? U
39+
: [];
40+
41+
/**
42+
Generate a union of all possible paths to properties in the given object.
43+
44+
It also works with arrays.
45+
46+
Use-case: You want a type-safe way to access deeply nested properties in an object.
47+
48+
@example
49+
```
50+
import type {Paths} from 'type-fest';
51+
52+
type Project = {
53+
filename: string;
54+
listA: string[];
55+
listB: [{filename: string}];
56+
folder: {
57+
subfolder: {
58+
filename: string;
59+
};
60+
};
61+
};
62+
63+
type ProjectPaths = Paths<Project>;
64+
//=> 'filename' | 'listA' | 'listB' | 'folder' | `listA.${number}` | 'listB.0' | 'listB.0.filename' | 'folder.subfolder' | 'folder.subfolder.filename'
65+
66+
declare function open<Path extends ProjectPaths>(path: Path): void;
67+
68+
open('filename'); // Pass
69+
open('folder.subfolder'); // Pass
70+
open('folder.subfolder.filename'); // Pass
71+
open('foo'); // TypeError
72+
73+
// Also works with arrays
74+
open('listA.1'); // Pass
75+
open('listB.0'); // Pass
76+
open('listB.1'); // TypeError. Because listB only has one element.
77+
```
78+
79+
@category Object
80+
@category Array
81+
*/
82+
export type Paths<T extends UnknownRecord | UnknownArray> =
83+
IsAny<T> extends true
84+
? never
85+
: T extends UnknownArray
86+
? number extends T['length']
87+
// We need to handle the fixed and non-fixed index part of the array separately.
88+
? InternalPaths<FilterFixedIndexArray<T>>
89+
| InternalPaths<Array<FilterNotFixedIndexArray<T>[number]>>
90+
: InternalPaths<T>
91+
: InternalPaths<T>;
92+
93+
export type InternalPaths<_T extends UnknownRecord | UnknownArray, T = Required<_T>> =
94+
T extends EmptyObject | readonly []
95+
? never
96+
: {
97+
[Key in keyof T]:
98+
Key extends string | number // Limit `Key` to string or number.
99+
? T[Key] extends UnknownRecord | UnknownArray
100+
? (
101+
IsNever<Paths<T[Key]>> extends false
102+
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` do not work.
103+
? Key | ToString<Key> | `${Key}.${Paths<T[Key]>}`
104+
: Key | ToString<Key>
105+
)
106+
: Key | ToString<Key>
107+
: never
108+
}[keyof T & (T extends UnknownArray ? number : unknown)];

‎test-d/paths.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {expectType} from 'tsd';
2+
import type {Paths} from '../index';
3+
4+
declare const normal: Paths<{foo: string}>;
5+
expectType<'foo'>(normal);
6+
7+
type DeepObject = {
8+
a: {
9+
b: {
10+
c: {
11+
d: string;
12+
};
13+
};
14+
b2: number[];
15+
b3: boolean;
16+
};
17+
};
18+
declare const deepObject: Paths<DeepObject>;
19+
expectType<'a' | 'a.b' | 'a.b2' | 'a.b3' | 'a.b.c' | 'a.b.c.d' | `a.b2.${number}`>(deepObject);
20+
21+
declare const emptyObject: Paths<{}>;
22+
expectType<never>(emptyObject);
23+
24+
declare const emptyArray: Paths<[]>;
25+
expectType<never>(emptyArray);
26+
27+
declare const symbol: Paths<{[Symbol.iterator]: string}>;
28+
expectType<never>(symbol);
29+
30+
declare const never: Paths<never>;
31+
expectType<never>(never);
32+
33+
declare const date: Paths<{foo: Date}>;
34+
expectType<'foo'>(date);
35+
36+
declare const mixed: Paths<{foo: boolean} | {bar: string}>;
37+
expectType<'foo' | 'bar'>(mixed);
38+
39+
declare const array: Paths<Array<{foo: string}>>;
40+
expectType<number | `${number}` | `${number}.foo`>(array);
41+
42+
declare const tuple: Paths<[{foo: string}]>;
43+
expectType<'0' | '0.foo'>(tuple);
44+
45+
declare const deeplist: Paths<{foo: Array<{bar: boolean[]}>}>;
46+
expectType<'foo' | `foo.${number}` | `foo.${number}.bar` | `foo.${number}.bar.${number}`>(deeplist);
47+
48+
declare const readonly: Paths<{foo: Readonly<{bar: string}>}>;
49+
expectType<'foo' | 'foo.bar'>(readonly);
50+
51+
declare const readonlyArray: Paths<{foo: readonly string[]}>;
52+
expectType<'foo' | `foo.${number}`>(readonlyArray);
53+
54+
declare const optional: Paths<{foo?: {bar?: number}}>;
55+
expectType<'foo' | 'foo.bar'>(optional);
56+
57+
declare const record: Paths<Record<'a', any>>;
58+
expectType<'a'>(record);
59+
60+
declare const record2: Paths<Record<1, unknown>>;
61+
expectType<1 | '1'>(record2);
62+
63+
// Test for unknown length array
64+
declare const trailingSpreadTuple: Paths<[{a: string}, ...Array<{b: number}>]>;
65+
expectType<number | `${number}` | '0.a' | `${number}.b`>(trailingSpreadTuple);
66+
67+
declare const trailingSpreadTuple1: Paths<[{a: string}, {b: number}, ...Array<{c: number}>]>;
68+
expectType<number | `${number}` | '0.a' | `${number}.b`>(trailingSpreadTuple);
69+
expectType<number | `${number}` | '0.a' | '1.b' | `${number}.c`>(trailingSpreadTuple1);
70+
71+
declare const leadingSpreadTuple: Paths<[...Array<{a: string}>, {b: number}]>;
72+
expectType<number | `${number}` | `${number}.b` | `${number}.a`>(leadingSpreadTuple);
73+
74+
declare const leadingSpreadTuple1: Paths<[...Array<{a: string}>, {b: number}, {c: number}]>;
75+
expectType<number | `${number}` | `${number}.b` | `${number}.c` | `${number}.a`>(leadingSpreadTuple1);

0 commit comments

Comments
 (0)
Please sign in to comment.