type-fest
Version:
A collection of essential TypeScript types
242 lines (195 loc) • 6.36 kB
TypeScript
import type {NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions, MapsSetsOrArrays} from './internal/index.d.ts';
import type {IsAny} from './is-any.d.ts';
import type {UnknownArray} from './unknown-array.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.
@see {@link Paths}
*/
export type PathsOptions = {
/**
The maximum depth to recurse when searching for paths. Range: 0 ~ 10.
@default 5
*/
maxRecursionDepth?: number;
/**
Use bracket notation for array indices and numeric object keys.
@default false
@example
```
import type {Paths} from 'type-fest';
type ArrayExample = {
array: ['foo'];
};
type A = Paths<ArrayExample, {bracketNotation: false}>;
//=> 'array' | 'array.0'
type B = Paths<ArrayExample, {bracketNotation: true}>;
//=> 'array' | 'array[0]'
```
@example
```
import type {Paths} from 'type-fest';
type NumberKeyExample = {
1: ['foo'];
};
type A = Paths<NumberKeyExample, {bracketNotation: false}>;
//=> 1 | '1' | '1.0'
type B = Paths<NumberKeyExample, {bracketNotation: true}>;
//=> '[1]' | '[1][0]'
```
*/
bracketNotation?: boolean;
/**
Only include leaf paths in the output.
@default false
@example
```
import type {Paths} from 'type-fest';
type Post = {
id: number;
author: {
id: number;
name: {
first: string;
last: string;
};
};
};
type AllPaths = Paths<Post, {leavesOnly: false}>;
//=> 'id' | 'author' | 'author.id' | 'author.name' | 'author.name.first' | 'author.name.last'
type LeafPaths = Paths<Post, {leavesOnly: true}>;
//=> 'id' | 'author.id' | 'author.name.first' | 'author.name.last'
```
@example
```
import type {Paths} from 'type-fest';
type ArrayExample = {
array: Array<{foo: string}>;
tuple: [string, {bar: string}];
};
type AllPaths = Paths<ArrayExample, {leavesOnly: false}>;
//=> 'array' | 'tuple' | `array.${number}` | `array.${number}.foo` | 'tuple.0' | 'tuple.1' | 'tuple.1.bar'
type LeafPaths = Paths<ArrayExample, {leavesOnly: true}>;
//=> `array.${number}.foo` | 'tuple.0' | 'tuple.1.bar'
```
*/
leavesOnly?: boolean;
/**
Only include paths at the specified depth. By default all paths up to {@link PathsOptions.maxRecursionDepth | `maxRecursionDepth`} are included.
Note: Depth starts at `0` for root properties.
@default number
@example
```
import type {Paths} from 'type-fest';
type Post = {
id: number;
author: {
id: number;
name: {
first: string;
last: string;
};
};
};
type DepthZero = Paths<Post, {depth: 0}>;
//=> 'id' | 'author'
type DepthOne = Paths<Post, {depth: 1}>;
//=> 'author.id' | 'author.name'
type DepthTwo = Paths<Post, {depth: 2}>;
//=> 'author.name.first' | 'author.name.last'
type LeavesAtDepthOne = Paths<Post, {leavesOnly: true; depth: 1}>;
//=> 'author.id'
```
*/
depth?: number;
};
type DefaultPathsOptions = {
maxRecursionDepth: 5;
bracketNotation: false;
leavesOnly: false;
depth: number;
};
/**
Generate a union of all possible paths to properties in the given object.
It also works with arrays.
Use-case: You want a type-safe way to access deeply nested properties in an object.
@example
```
import type {Paths} from 'type-fest';
type Project = {
filename: string;
listA: string[];
listB: [{filename: string}];
folder: {
subfolder: {
filename: string;
};
};
};
type ProjectPaths = Paths<Project>;
//=> 'filename' | 'listA' | 'listB' | 'folder' | `listA.${number}` | 'listB.0' | 'listB.0.filename' | 'folder.subfolder' | 'folder.subfolder.filename'
declare function open<Path extends ProjectPaths>(path: Path): void;
open('filename'); // Pass
open('folder.subfolder'); // Pass
open('folder.subfolder.filename'); // Pass
// @ts-expect-error
open('foo'); // TypeError
// Also works with arrays
open('listA.1'); // Pass
open('listB.0'); // Pass
// @ts-expect-error
open('listB.1'); // TypeError. Because listB only has one element.
```
@category Object
@category Array
*/
export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, Options>>;
type _Paths<T, Options extends Required<PathsOptions>, CurrentDepth extends number = 0> =
T extends NonRecursiveType | Exclude<MapsSetsOrArrays, UnknownArray>
? never
: IsAny<T> extends true
? never
: T extends object
? InternalPaths<Required<T>, Options, CurrentDepth>
: never;
type InternalPaths<T, Options extends Required<PathsOptions>, CurrentDepth extends number> =
{[Key in keyof T]: Key extends string | number // Limit `Key` to `string | number`
? (
And<Options['bracketNotation'], IsNumberLike<Key>> extends true
? `[${Key}]`
: CurrentDepth extends 0
// Return both `Key` and `ToString<Key>` because for number keys, like `1`, both `1` and `'1'` are valid keys.
? Key | ToString<Key>
: `.${(Key | ToString<Key>)}`
) extends infer TransformedKey extends string | number
? ((Options['leavesOnly'] extends true
? Options['maxRecursionDepth'] extends CurrentDepth
? TransformedKey
: IsNever<T[Key]> extends true
? TransformedKey
: T[Key] extends infer Value // For distributing `T[Key]`
? (Value extends readonly [] | NonRecursiveType | Exclude<MapsSetsOrArrays, UnknownArray>
? TransformedKey
: IsNever<keyof Value> extends true // Check for empty object & `unknown`, because `keyof unknown` is `never`.
? TransformedKey
: never)
: never // Should never happen
: TransformedKey
) extends infer _TransformedKey
// If `depth` is provided, the condition becomes truthy only when it matches `CurrentDepth`.
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
? CurrentDepth extends Options['depth']
? _TransformedKey
: never
: never)
// Recursively generate paths for the current key
| (GreaterThan<Options['maxRecursionDepth'], CurrentDepth> extends true // Limit the depth to prevent infinite recursion
? `${TransformedKey}${_Paths<T[Key], Options, Sum<CurrentDepth, 1>> & (string | number)}`
: never)
: never
: never
}[keyof T & (T extends UnknownArray ? number : unknown)];
export {};