UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

284 lines (283 loc) 13.2 kB
import type { Form as FormNS, Field as FieldNS, Value as ValueNS, Iterate as IterateNS } from './'; import type { JsonObject } from './utils/json-pointer'; /** * Type-level helpers for type-checking Eufemia Forms `path` props against your * data model. Register your data type once via {@link Register}, then use the * pre-typed `RegisteredField`/`RegisteredValue`/`RegisteredForm` namespaces for * autocomplete and compile-time `path` checking. * * @example * declare module '@dnb/eufemia/extensions/forms' { * interface Register { * formData: MyData * } * } * * import { * RegisteredForm as Form, * RegisteredField as Field, * } from '@dnb/eufemia/extensions/forms' * * <Field.Name.First path="/firstName" /> // typed; typos are a compile error */ /** * Depth limiter for the recursive `Paths` type, to avoid * "Type instantiation is excessively deep and possibly infinite" on * deep or recursive data models. */ type Prev = [never, 0, 1, 2, 3, 4, 5, 6]; /** * Produces a union of all valid JSON Pointer paths in `Data`. * * @example * type T = { user: { name: string }; items: Array<{ title: string }> } * type P = Paths<T> * // "/user" | "/user/name" | "/items" | `/items/${number}` | `/items/${number}/title` */ export type Paths<Data> = PathsImpl<Data, 6>; /** * Recursive implementation of {@link Paths}, carrying the depth limiter so the * public `Paths` type stays a single-argument helper. */ type PathsImpl<Data, Depth extends number> = [Depth] extends [never] ? never : Data extends ReadonlyArray<infer Item> ? `/${number}` | `/${number}${PathsImpl<Item, Prev[Depth]>}` : Data extends object ? { [Key in Extract<keyof Data, string>]: `/${Key}` | `/${Key}${PathsImpl<NonNullable<Data[Key]>, Prev[Depth]>}`; }[Extract<keyof Data, string>] : never; /** * Resolves the value type located at the JSON Pointer path `P` in `Data`. * * @example * type T = { user: { age: number } } * type V = PathValue<T, '/user/age'> // number */ export type PathValue<Data, P> = P extends `/${infer Key}/${infer Rest}` ? Key extends `${number}` ? Data extends ReadonlyArray<infer Item> ? PathValue<Item, `/${Rest}`> : never : Key extends keyof Data ? PathValue<NonNullable<Data[Key]>, `/${Rest}`> : never : P extends `/${infer Key}` ? Key extends `${number}` ? Data extends ReadonlyArray<infer Item> ? Item : never : Key extends keyof Data ? Data[Key] : never : never; /** * The subset of {@link Paths} in `Data` that point to an array. Used to narrow * the `path` prop of `Iterate.Array` to the valid array paths. */ type ArrayPaths<Data> = { [P in Paths<Data>]: NonNullable<PathValue<Data, P>> extends ReadonlyArray<unknown> ? P : never; }[Paths<Data>]; /** * The subset of {@link Paths} in `Data` that point to a (non-array) object. * Used to narrow the `path` prop of `Form.Section` to the valid object paths. */ type ObjectPaths<Data> = { [P in Paths<Data>]: NonNullable<PathValue<Data, P>> extends ReadonlyArray<unknown> ? never : NonNullable<PathValue<Data, P>> extends object ? P : never; }[Paths<Data>]; /** * The static members attached to a component (e.g. `Field.Address.Street`), * recursively re-typed so nested sub-components keep their `path` (and * optionally `itemPath`) narrowing. */ type TypedStatics<Component, Data, ItemData = never> = { [Key in keyof Component]: WithTypedPath<Component[Key], Data, ItemData>; }; /** * Narrows the item-relative `itemPath` prop to the valid paths of an Iterate * item type `ItemData`. A no-op when `ItemData` is `never` (the default), so * the common `path`-only narrowing leaves `itemPath` untouched. */ type NarrowItemPath<Props, ItemData> = [ItemData] extends [never] ? Props : Props extends { itemPath?: unknown; } ? Omit<Props, 'itemPath'> & { itemPath?: Paths<ItemData>; } : Props; /** * Re-types a single Field/Value component so its `path` prop is narrowed to * the valid `Paths<Data>` union. When an Iterate item type `ItemData` is given, * the item-relative `itemPath` prop is narrowed too. Components without a * `path` prop are left untouched. Static sub-members (such as * `Field.Address.Street`) are preserved and recursively narrowed as well. */ type WithTypedPath<Component, Data, ItemData = never> = Component extends (props: infer Props) => infer Return ? Props extends { path?: unknown; } ? ((props: NarrowItemPath<Omit<Props, 'path'> & { path?: Paths<Data>; }, ItemData>) => Return) & TypedStatics<Component, Data, ItemData> : Component : Component; /** * Maps over a Field/Value namespace, narrowing every member's `path` prop (and, * when `ItemData` is given, the item-relative `itemPath` prop). * * @internal Building block for the pre-typed namespaces; not part of the public API. */ export type TypedNamespace<Namespace, Data, ItemData = never> = { [Key in keyof Namespace]: WithTypedPath<Namespace[Key], Data, ItemData>; }; /** * Re-types a single container component (such as `Form.Section` or * `Iterate.Array`) so its own `path` prop is narrowed to `PathUnion`, while its * static sub-members are preserved unchanged. */ type NarrowContainerPath<Component, PathUnion> = Component extends (props: infer Props) => infer Return ? Props extends { path?: unknown; } ? ((props: Omit<Props, 'path'> & { path?: PathUnion; }) => Return) & { [Key in keyof Component]: Component[Key]; } : Component : Component; /** * Augmentable registry for binding a form data type globally. Augment it once * in your app, then use the pre-typed `RegisteredField`/`RegisteredValue`/ * `RegisteredForm` namespaces (and {@link TypedItemField}/ * {@link TypedSectionField} for relative paths) — they resolve the registered * data type automatically: * * @example * declare module '@dnb/eufemia/extensions/forms' { * interface Register { * formData: MyData * } * } * * // Anywhere afterwards: * import { * RegisteredForm as Form, * RegisteredField as Field, * } from '@dnb/eufemia/extensions/forms' * * <Field.Name.First path="/firstName" /> * * @remarks * This binds one data type globally. */ export interface Register { } /** * Resolves the form data type registered via {@link Register}. Falls back to * `JsonObject` when no `formData` has been registered. */ export type RegisteredFormData = Register extends { formData: infer Data extends JsonObject; } ? Data : JsonObject; /** * The type used for the `path` prop of every `Field.*` and `Value.*` component. * * When a form data type is registered via {@link Register}, this offers * **autocomplete** of the valid {@link Paths} directly on `Field.Name`, * `Value.String`, and so on. It still accepts any string, so relative paths * inside `Form.Section`/`Iterate` and dynamic paths keep working. Without a * registration it is a plain string, exactly as before. * * @remarks * Because any string is still accepted, typos are not hard errors here. For * compile-time errors on invalid paths, use the pre-typed `RegisteredField`/ * `RegisteredValue` namespaces instead. */ export type RegisteredPath = Register extends { formData: infer Data extends JsonObject; } ? Paths<Data> | (string & {}) : string; type TypedHandler<Data extends JsonObject> = (props: Parameters<typeof FormNS.Handler<Data>>[0]) => ReturnType<typeof FormNS.Handler<Data>>; /** * The `Form` namespace with its `Handler` bound to `Data` (so `Form.Handler` * gets typed `data`/`defaultData`) and `Form.Section`'s `path` prop narrowed to * the valid object paths of `Data`. The rest of the namespace (`Form.Card`, * `Form.SubmitButton`, …) stays available unchanged. Used internally to build * the pre-typed `RegisteredForm` namespace. * * @internal Building block for `RegisteredForm`; not part of the public API. */ export type TypedForm<Data extends JsonObject = RegisteredFormData> = Omit<typeof FormNS, 'Handler' | 'Section'> & { Handler: TypedHandler<Data>; Section: NarrowContainerPath<typeof FormNS.Section, [ ObjectPaths<Data> ] extends [never] ? Paths<Data> : ObjectPaths<Data>>; }; /** * The `Iterate` namespace with `Iterate.Array`'s `path` prop narrowed to the * valid array paths of `Data`. The rest of the namespace (`Iterate.PushButton`, * `Iterate.RemoveButton`, …) stays available unchanged. Used internally to * build the pre-typed `RegisteredIterate` namespace. * * @internal Building block for `RegisteredIterate`; not part of the public API. */ export type TypedIterate<Data extends JsonObject = RegisteredFormData> = Omit<typeof IterateNS, 'Array'> & { Array: NarrowContainerPath<typeof IterateNS.Array, [ ArrayPaths<Data> ] extends [never] ? Paths<Data> : ArrayPaths<Data>>; }; /** * The `Field` namespace with every member's `path` prop narrowed to the valid * paths of `Data` (defaults to the registered form data — see {@link Register}). * Used internally to build the pre-typed `RegisteredField` namespace. * * @internal Building block for `RegisteredField`; not part of the public API. */ export type TypedField<Data extends JsonObject = RegisteredFormData> = TypedNamespace<typeof FieldNS, Data>; /** * The `Value` namespace with every member's `path` prop narrowed to the valid * paths of `Data`. Used internally to build the pre-typed `RegisteredValue` * namespace. * * @internal Building block for `RegisteredValue`; not part of the public API. */ export type TypedValue<Data extends JsonObject = RegisteredFormData> = TypedNamespace<typeof ValueNS, Data>; /** * Resolves the item type of the array located at `ArrayPath` in the registered * form data. Falls back to `never` (which disables `itemPath` narrowing) when * the path does not point to an array of objects. */ type RegisteredItem<ArrayPath extends Paths<RegisteredFormData>> = NonNullable<PathValue<RegisteredFormData, ArrayPath>> extends ReadonlyArray<infer Item extends JsonObject> ? Item : never; /** * The `Field` namespace with every member's item-relative `itemPath` prop * narrowed to the valid paths of the array item at `ArrayPath` (resolved from * the registered form data — see {@link Register}). * * Use it inside an `Iterate.Array` so item-relative `itemPath` props are * type-checked. The item type is derived from the single registered root data, * so there is one source of truth — no extra registration is needed. Pass the * same path you give to `Iterate.Array`. * * @example * import { Field, Iterate } from '@dnb/eufemia/extensions/forms' * import type { TypedItemField } from '@dnb/eufemia/extensions/forms' * * // Registered data: { accounts: Array<{ name: string }> } * const { String } = Field as TypedItemField<'/accounts'> * * <Iterate.Array path="/accounts"> * <String itemPath="/name" /> // typed + checked * </Iterate.Array> */ export type TypedItemField<ArrayPath extends Paths<RegisteredFormData>> = TypedNamespace<typeof FieldNS, RegisteredFormData, RegisteredItem<ArrayPath>>; /** * The `Value` namespace with every member's item-relative `itemPath` prop * narrowed to the valid paths of the array item at `ArrayPath`. See * {@link TypedItemField}. */ export type TypedItemValue<ArrayPath extends Paths<RegisteredFormData>> = TypedNamespace<typeof ValueNS, RegisteredFormData, RegisteredItem<ArrayPath>>; /** * Resolves the object type located at `SectionPath` in the registered form * data, used to narrow the section-relative `path` prop. Falls back to `never` * when the path does not point to an object. */ type RegisteredSection<SectionPath extends Paths<RegisteredFormData>> = NonNullable<PathValue<RegisteredFormData, SectionPath>> extends infer Section extends JsonObject ? Section : never; /** * The `Field` namespace with every member's `path` prop narrowed to the valid * paths of the object at `SectionPath` (resolved from the registered form data * — see {@link Register}). * * Use it inside a `Form.Section` so the section-relative `path` props are * type-checked. Inside a section, fields use a `path` relative to the section, * so the type is derived from the section's object type — not the root. The * object type comes from the single registered root data, so there is one * source of truth — no extra registration is needed. Pass the same path you * give to `Form.Section`. * * @example * import { Field, Form } from '@dnb/eufemia/extensions/forms' * import type { TypedSectionField } from '@dnb/eufemia/extensions/forms' * * // Registered data: { company?: { name: string } } * const { Name } = Field as TypedSectionField<'/company'> * * <Form.Section path="/company"> * <Name.Company path="/name" /> // typed + checked, relative to the section * </Form.Section> */ export type TypedSectionField<SectionPath extends Paths<RegisteredFormData>> = TypedNamespace<typeof FieldNS, RegisteredSection<SectionPath>>; /** * The `Value` namespace with every member's `path` prop narrowed to the valid * paths of the object at `SectionPath`. See {@link TypedSectionField}. */ export type TypedSectionValue<SectionPath extends Paths<RegisteredFormData>> = TypedNamespace<typeof ValueNS, RegisteredSection<SectionPath>>; export {};