UNPKG

@open-formulieren/formio-builder

Version:

An opinionated Formio webform builder for Open Forms

64 lines (63 loc) 3.69 kB
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 {};