// Thanks to https://stackoverflow.com/a/77451367/70894
type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false;
export type AllKeys<T> = T extends T ? keyof T : never;
type PickType<T, K extends AllKeys<T>> = T extends { [k in K]: infer Item } ? Item : never;
// Thanks to https://dev.to/lucianbc/union-type-merging-in-typescript-9al
export type MergeUnion<T> = {
[K in AllKeys<T>]: PickType<T, K>;
};
type DictOrArray = Record<PropertyKey, unknown> | unknown[];
/**
* Lists all paths in an object as string accessors.
*/
export type FormPath<T extends DictOrArray, Type = any> = string &
StringPath<T, { filter: "all"; objAppend: never; path: ""; type: Type }>;
/**
* List paths in an object as string accessors, but only with non-objects as accessible properties.
* Similar to the leaves in a node tree, if you look at the object as a tree structure.
*/
export type FormPathLeaves<T extends DictOrArray, Type = any> = string &
StringPath<T, { filter: "leaves"; objAppend: never; path: ""; type: Type }>;
/**
* List paths in an object as string accessors, but only with non-objects as accessible properties.
* Also includes the _errors field for objects and arrays.
*/
export type FormPathLeavesWithErrors<T extends DictOrArray, Type = any> = string &
StringPath<T, { filter: "leaves"; objAppend: "_errors"; path: ""; type: Type }>;
/**
* List all arrays in an object as string accessors.
*/
export type FormPathArrays<T extends object, Type = any> = string &
StringPath<
T,
{
filter: 'arrays';
objAppend: never;
path: '';
type: Type extends any[] ? Type : Type[];
}
>;
type Concat<Path extends string, Next extends string> = `${Path}${Path extends "" ? "" : "."}${Next}`;
type StringPathOptions = {
filter: "arrays" | "leaves" | "all";
objAppend: string | never;
path: string;
type: any;
};
type If<
Options extends StringPathOptions,
Pred extends keyof Options,
Subj,
Then,
Else,
Value
> = Options[Pred] extends Subj
? Options["type"] extends never
? Then
: Value extends Options["type"]
? Then
: never
: Else;
type StringPath<
T extends DictOrArray,
Options extends StringPathOptions = {
filter: "all";
objAppend: never;
path: "";
type: never;
}
// If T is an array, infer U
> = T extends (infer U)[]
? // If objAppend is a string, return the path with objAppend appended, or never
| If<Options, "objAppend", string, Concat<Options["path"], Options["objAppend"]>, never, T>
// If filter is "arrays" or "all", return the path as is, or never
| If<Options, "filter", "arrays" | "all", Options["path"], never, T>
// If U is a record or array
| (NonNullable<U> extends DictOrArray
? // Recursively call StringPath on U
StringPath<
NonNullable<U>,
{
filter: Options["filter"];
objAppend: Options["objAppend"];
path: `${Options["path"]}[${number}]`;
type: Options["type"];
}
>
: // Otherwise, if the filter is "leaves" or "all", return the path with the array
// index appended, or never
If<Options, "filter", "leaves" | "all", `${Options["path"]}[${number}]`, never, T>)
: // Otherwise, T is a record. Iterate over the keys of T
{
// If T[K] is a record or array
[K in Extract<AllKeys<T>, string>]: NonNullable<T[K]> extends DictOrArray
? // If objAppend is a string, return the path with objAppend appended, or never
| If<Options, "objAppend", string, Concat<Options["path"], Options["objAppend"]>, never, T[K]>
// If T[K] is an array, infer U
| NonNullable<T[K]> extends (infer U)[]
? // If filter is "arrays" or "all", return the path with the key appended, or never
| If<Options, "filter", "arrays" | "all", Concat<Options["path"], K>, never, T[K]>
// If U, the array element, is also an array
| (NonNullable<U> extends unknown[]
? // If filter is "arrays" or "all", return the path with the key and array index appended, or never
If<Options, "filter", "arrays" | "all", Concat<Options["path"], `${K}[${number}]`>, never, T[K]>
: // Otherwise, if U is a record
NonNullable<U> extends DictOrArray
? // If T[K], which is an array of records, is any
IsAny<T[K]> extends true
? // Return the path with the key and index appended
Concat<Options["path"], `${K}[${number}]`>
: // If filter is "all", return the path with the key and index appended, or never
If<Options, "filter", "all", Concat<Options["path"], `${K}[${number}]`>, never, U>
: // Otherwise, if U is not a record or array, and filter is "leaves" or "all",
// return the path with the key and index appended, or never
If<Options, "filter", "leaves" | "all", Concat<Options["path"], `${K}[${number}]`>, never, U>)
// If U is a record or array
| (NonNullable<U> extends DictOrArray
? // Recursively call StringPath on U
StringPath<
NonNullable<U>,
{
filter: Options["filter"];
objAppend: Options["objAppend"];
path: Concat<Options["path"], `${K}[${number}]`>;
type: Options["type"];
}
>
: never)
: // If T[K] is any
IsAny<T[K]> extends true
? // Append the key to the path
Concat<Options["path"], K>
: // Otherwise, if filter is "all", return the path with the key appended, or never
| If<
Options,
"filter",
"all",
Concat<Options["path"], K>,
unknown extends T[K] ? Concat<Options["path"], K> : never,
T[K]
>
// And recursively call StringPath on T[K]
| StringPath<
NonNullable<T[K]>,
{
filter: Options["filter"];
objAppend: Options["objAppend"];
path: Concat<Options["path"], K>;
type: Options["type"];
}
>
: // Otherwise, if T[K] is not a record or array, and filter is "leaves" or "all",
// return the path with the key appended, or never
If<Options, "filter", "leaves" | "all", Concat<Options["path"], K>, never, T[K]>;
// Extract the values of the keys in the object
}[Extract<AllKeys<T>, string>];
export type FormPathType<T, P extends string> = P extends keyof T
? T[P]
: P extends number
? T
: P extends `.${infer Rest}`
? FormPathType<NonNullable<T>, Rest>
: P extends `${number}]${infer Rest}`
? NonNullable<T> extends (infer U)[]
? FormPathType<U, Rest>
: { invalid_path1: P; Type: T }
: P extends `${infer K}[${infer Rest}`
? K extends keyof NonNullable<T>
? FormPathType<NonNullable<T>[K], Rest>
: FormPathType<T, Rest>
: P extends `${infer K}.${infer Rest}`
? K extends keyof NonNullable<T>
? FormPathType<NonNullable<T>[K], Rest>
: NonNullable<T> extends (infer U)[]
? FormPathType<U, Rest>
: { invalid_path2: P; Type: T }
: P extends `[${infer K}].${infer Rest}`
? K extends number
? T extends (infer U)[]
? FormPathType<U, Rest>
: { invalid_path3: P; Type: T }
: P extends `${number}`
? NonNullable<T> extends (infer U)[]
? U
: { invalid_path4: P; Type: T }
: P extends keyof NonNullable<T>
? NonNullable<T>[P]
: P extends `${number}`
? NonNullable<T> extends (infer U)[]
? U
: { invalid_path5: P; Type: T }
: { invalid_path6: P; Type: T }
: P extends ""
? T
: P extends AllKeys<T>
? MergeUnion<T>[P]
: { invalid_path7: P; Type: T };