From 8beff9bb03b6ed66c1308d6a2e6af6475a62390b Mon Sep 17 00:00:00 2001 From: Miroslav Petrik Date: Mon, 4 Dec 2023 15:53:10 +0100 Subject: [PATCH] fix(types): `listFieldBuilder` with proper type for primitive and fields fixes #56 #hacky --- .../list-field/listFieldBuilder.test-d.ts | 60 +++++++++++++++---- src/fields/list-field/listFieldBuilder.ts | 45 ++++++++++---- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/fields/list-field/listFieldBuilder.test-d.ts b/src/fields/list-field/listFieldBuilder.test-d.ts index 8eb3d22..343720f 100644 --- a/src/fields/list-field/listFieldBuilder.test-d.ts +++ b/src/fields/list-field/listFieldBuilder.test-d.ts @@ -1,19 +1,55 @@ -import { expectTypeOf, test } from "vitest"; +import { describe, expectTypeOf, test } from "vitest"; import { listFieldBuilder } from "./listFieldBuilder"; -import { textField } from "../"; +import { type TextField, textField } from "../"; -test("listFieldBuilder - cannot build with random data", () => { - const addressBuilder = listFieldBuilder(({ street }) => ({ - street: textField({ name: "street", value: street }), - })); +describe("listFieldBuilder", () => { + describe("when building list of primitive atoms", () => { + const positivesBuilder = listFieldBuilder((positive) => + textField({ name: "positive", value: positive }), + ); - expectTypeOf(addressBuilder).toBeCallableWith([{ street: "foo" }]); + test("empty call produces single item", () => { + const single = positivesBuilder(); - // Doesnt work for no-argument (due to function overload) - // https://github.com/mmkal/expect-type/issues/30 - // expectTypeOf(addressBuilder).toBeCallableWith(); + expectTypeOf(single).toEqualTypeOf(); + }); - // TODO: expect-type issue - // expectTypeOf(addressBuilder).not.toBeCallableWith([{ notStreet: "foo" }]); + test("call with array produces list of items", () => { + // NOTE: the undefined is simply empty value + const single = positivesBuilder(["pretty", "fast", undefined]); + + expectTypeOf(single).toEqualTypeOf(); + }); + }); + + describe("when building list of form fields", () => { + const addressBuilder = listFieldBuilder(({ street }) => ({ + street: textField({ name: "street", value: street }), + })); + + test("cannot build with random data", () => { + expectTypeOf(addressBuilder).toBeCallableWith([{ street: "foo" }]); + + // Doesnt work for no-argument (due to function overload) + // https://github.com/mmkal/expect-type/issues/30 + // expectTypeOf(addressBuilder).toBeCallableWith(); + + // TODO: expect-type issue + // expectTypeOf(addressBuilder).not.toBeCallableWith([{ notStreet: "foo" }]); + }); + + test("empty call produces single item", () => { + const single = addressBuilder(); + + expectTypeOf(single).toEqualTypeOf<{ street: TextField }>(); + }); + + test("call with array produces list of items", () => { + // NOTE: the undefined is simply empty value + const multi = addressBuilder([{ street: "Hrad" }, { street: undefined }]); + + expectTypeOf(multi).toEqualTypeOf<{ street: TextField }[]>(); + }); + }); }); diff --git a/src/fields/list-field/listFieldBuilder.ts b/src/fields/list-field/listFieldBuilder.ts index 057e6ef..97ec4fd 100644 --- a/src/fields/list-field/listFieldBuilder.ts +++ b/src/fields/list-field/listFieldBuilder.ts @@ -1,36 +1,55 @@ import { FieldAtom, FormFieldValues, FormFields } from "form-atoms"; -import { ExtractAtomValue } from "jotai"; -type FieldAtomValue> = ExtractAtomValue< - ExtractAtomValue["value"] ->; +type EmptyValues = { + [Key in keyof Fields]: Fields[Key] extends FieldAtom + ? undefined + : Fields[Key] extends FormFields + ? EmptyValues + : Fields[Key] extends Array + ? Item extends FieldAtom + ? undefined[] + : Item extends FormFields + ? EmptyValues[] + : never + : never; +}; type ListFieldItems = FieldAtom | FormFields; -type ListFieldValue = T extends FieldAtom - ? FieldAtomValue +type ListFieldValues = T extends FieldAtom + ? Value | undefined // also empty : T extends FormFields - ? FormFieldValues + ? FormFieldValues | EmptyValues : never; // actual type must be one of overloads, as this one is ignored export function listFieldBuilder< Fields extends ListFieldItems, - Value = ListFieldValue, ->(builder: (value: Value) => Fields) { - let emptyValue: undefined | Value = undefined; + /** + * HACK + * Having the Values computed in generic argument, + * fixes the return types **when the argument to builder is used**. + */ + Values extends ListFieldValues, +>(builder: (value: Values) => Fields) { + let emptyValue: undefined | Values = undefined; try { // test if builder is 'atomBuilder', e.g. returns plain atom // @ts-expect-error this is a test call builder(undefined); } catch { // builder is 'fieldsBuilder', e.g. it returns Record - emptyValue = {} as Value; + emptyValue = {} as Values; } function buildFields(): Fields; - function buildFields(data: ListFieldValue[]): Fields[]; - function buildFields(data?: ListFieldValue[]) { + /** + * @FIXME + * HACK2: + * the data is not simply Values, as that would produce the any[] on the returned function. + */ + function buildFields(data: ListFieldValues[]): Fields[]; + function buildFields(data?: Values[]) { if (data) { return data.map(builder); } else {