@open-formulieren/formio-builder
Version:
An opinionated Formio webform builder for Open Forms
64 lines (63 loc) • 3.69 kB
TypeScript
type Primitive = string | number | boolean | undefined | null;
/**
* Constructs a union of possible paths (as dotted strings) into a given object type.
*
* This is intended to take any of the `AnyComponentSchema` members and use it to
* perform prop validation in React components, for example.
*
* Okay, so how does it work?
*
* - First, we check if the type var is a primitive - mapping over those doesn't make
* any sense as there is nothing to dive into. If it is primitive, we hit the 'never'
* branch and evaluation stops.
*
* - Additionally, this conditional type causes schemas that are a union to be mapped
* over, rather than the [keyof T] indexed access being limited to the *intersection*
* of common keys in all union members. So, this ensures that all the discriminated
* union members are processed individually.
*
* - Once inside the first conditional, we now know T is an object and apply a mapped
* type over its keys. If a key is optional, it's made required (that's the `-?`
* modifier), to ensure we loop over all possible keys, required or optional.
*
* - the values in the mapped type are checked first with a conditional type - we only
* care about string keys (because JSON serializable data is expected), so this does
* not return array indices either (by design - we handle an array of values as a
* single value in Formik), that's the `K extends string`. Possibly in the future we
* can extend this to return a `foo[${number}].bar` type to allow specific indices,
* but that's currently out of scope.
*
* When we have a string key, we check if we need to recurse by looking at the value
* type. The value type is looked up via `T[K]`. If it's not a (nested) object, the
* value returns just the key `K`. So for a type `{a: string}`, this mapped type
* results in `{a: 'a'}`. We ignore functions, since those are not values that can
* be set or managed through HTML forms.
*
* If the value type is an object though, then we recurse and we prefix the current
* key. A type `{a: {b: string}}` then becomes (in steps):
*
* 1. `{a: 'a' | a.Paths<{b: string}>}`
* 2. `{a: 'a' | 'a.b'}` (because b only has a leaf node and returns `'b'``)
*
* - 'finally', we grab all the values of the mapped type as a union via `[keyof T]`,
* so this turns the mapped type `{a: 'a', b: 'b'}` into the union `'a' | 'b'`
*
* When dealing with recursion/nested objects, the progression sort of looks like
* the following for the example type `{a: {b: string, c: string}}`
*
* 1. `{a: 'a' | <prefix-and-recurse for key a>}`
* 2. recurse for a: `{b: 'b', c: 'c'}
* 3. grab the values (end of recursion reached): `{b: 'b', c: 'c'}['b' | 'c']` -> `'b' | 'c'`
* 4. exit recursion, apply the prefix on the recursion result: `'a.b' | 'a.c'`
* 5. join the access paths for a and from a: `{a: 'a' | ('a.b' | 'a.c')}`
* 6. grab the values via the indexed access: `{a: 'a' | ('a.b' | 'a.c')}['a']`
* 7. final result: `'a' | 'a.b' | 'a.c'`
*/
export type Paths<T> = T extends Primitive | Primitive[] ? never : {
[K in keyof T]-?: K extends string ? T[K] extends Primitive | Primitive[] ? K : T[K] extends (...args: never[]) => unknown ? never : K | `${K}.${Paths<T[K]>}` : never;
}[keyof T];
export type GetValueAtPath<T, Path extends string> = T extends T ? Path extends `${infer P}.${infer Rest}` ? P extends keyof T ? GetValueAtPath<T[P], Rest> : never : Path extends keyof T ? T[Path] : never : never;
export type PathsForValueType<T, V> = keyof {
[P in Paths<T> as GetValueAtPath<T, P> extends V ? P : never]: true;
};
export {};