Created
July 20, 2023 04:06
-
-
Save csandman/ee0de7226b6f8b154e82fe539adab22a to your computer and use it in GitHub Desktop.
The `Path` TypeScript utility function used in react-hook-form to generate a type for every possible object dot path
This file contains 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
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
type Primitive = null | undefined | string | number | boolean | symbol | bigint; | |
/** | |
* Checks whether T1 can be exactly (mutually) assigned to T2 | |
* @typeParam T1 - type to check | |
* @typeParam T2 - type to check against | |
* ``` | |
* IsEqual<string, string> = true | |
* IsEqual<'foo', 'foo'> = true | |
* IsEqual<string, number> = false | |
* IsEqual<string, number> = false | |
* IsEqual<string, 'foo'> = false | |
* IsEqual<'foo', string> = false | |
* IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean | |
* ``` | |
*/ | |
type IsEqual<T1, T2> = T1 extends T2 | |
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2 | |
? true | |
: false | |
: false; | |
interface File extends Blob { | |
readonly lastModified: number; | |
readonly name: string; | |
} | |
interface FileList { | |
readonly length: number; | |
item(index: number): File | null; | |
[index: number]: File; | |
} | |
type BrowserNativeObject = Date | FileList | File; | |
/** | |
* Type to query whether an array type T is a tuple type. | |
* @typeParam T - type which may be an array or tuple | |
* @example | |
* ``` | |
* IsTuple<[number]> = true | |
* IsTuple<number[]> = false | |
* ``` | |
*/ | |
type IsTuple<T extends ReadonlyArray<any>> = number extends T['length'] | |
? false | |
: true; | |
/** | |
* Type which given a tuple type returns its own keys, i.e. only its indices. | |
* @typeParam T - tuple type | |
* @example | |
* ``` | |
* TupleKeys<[number, string]> = '0' | '1' | |
* ``` | |
*/ | |
type TupleKeys<T extends ReadonlyArray<any>> = Exclude<keyof T, keyof any[]>; | |
/** | |
* Helper function to break apart T1 and check if any are equal to T2 | |
* | |
* See {@link IsEqual} | |
*/ | |
type AnyIsEqual<T1, T2> = T1 extends T2 | |
? IsEqual<T1, T2> extends true | |
? true | |
: never | |
: never; | |
/** | |
* Helper type for recursively constructing paths through a type. | |
* This actually constructs the strings and recurses into nested | |
* object types. | |
* | |
* See {@link Path} | |
*/ | |
type PathImpl<K extends string | number, V, TraversedTypes> = V extends | |
| Primitive | |
| BrowserNativeObject | |
? `${K}` | |
: // Check so that we don't recurse into the same type | |
// by ensuring that the types are mutually assignable | |
// mutually required to avoid false positives of subtypes | |
true extends AnyIsEqual<TraversedTypes, V> | |
? `${K}` | |
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`; | |
/** | |
* Type which can be used to index an array or tuple type. | |
*/ | |
type ArrayKey = number; | |
/** | |
* Helper type for recursively constructing paths through a type. | |
* This obscures the internal type param TraversedTypes from exported contract. | |
* | |
* See {@link Path} | |
*/ | |
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V> | |
? IsTuple<T> extends true | |
? { | |
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>; | |
}[TupleKeys<T>] | |
: PathImpl<ArrayKey, V, TraversedTypes> | |
: { | |
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>; | |
}[keyof T]; | |
/** | |
* Type which eagerly collects all paths through a type | |
* @typeParam T - type which should be introspected | |
* @example | |
* ``` | |
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar' | |
* ``` | |
* | |
* --- | |
* | |
* This type is borrowed directly from react-hook-form | |
* | |
* @see {@link https://github.com/react-hook-form/react-hook-form/blob/011fad503cc8d4543892f8e847b9bd58c1d9400f/src/types/path/eager.ts#L51-L61} | |
*/ | |
// We want to explode the union type and process each individually | |
// so assignable types don't leak onto the stack from the base. | |
export type Path<T> = T extends any ? PathInternal<T> : never; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment