Skip to content

Commit

Permalink
fix(types): listFieldBuilder with proper type for primitive and fields
Browse files Browse the repository at this point in the history
fixes #56  #hacky
  • Loading branch information
MiroslavPetrik committed Dec 4, 2023
1 parent 3503913 commit 8beff9b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 25 deletions.
60 changes: 48 additions & 12 deletions src/fields/list-field/listFieldBuilder.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<TextField>();
});

// 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<TextField[]>();
});
});

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 }[]>();
});
});
});
45 changes: 32 additions & 13 deletions src/fields/list-field/listFieldBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,55 @@
import { FieldAtom, FormFieldValues, FormFields } from "form-atoms";
import { ExtractAtomValue } from "jotai";

type FieldAtomValue<T extends FieldAtom<any>> = ExtractAtomValue<
ExtractAtomValue<T>["value"]
>;
type EmptyValues<Fields extends FormFields> = {
[Key in keyof Fields]: Fields[Key] extends FieldAtom<any>
? undefined
: Fields[Key] extends FormFields
? EmptyValues<Fields[Key]>
: Fields[Key] extends Array<infer Item>
? Item extends FieldAtom<any>
? undefined[]
: Item extends FormFields
? EmptyValues<Item>[]
: never
: never;
};

type ListFieldItems = FieldAtom<any> | FormFields;

type ListFieldValue<T extends ListFieldItems> = T extends FieldAtom<any>
? FieldAtomValue<T>
type ListFieldValues<T> = T extends FieldAtom<infer Value>
? Value | undefined // also empty
: T extends FormFields
? FormFieldValues<T>
? FormFieldValues<T> | EmptyValues<T>
: never;

// actual type must be one of overloads, as this one is ignored
export function listFieldBuilder<
Fields extends ListFieldItems,
Value = ListFieldValue<Fields>,
>(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<Fields>,
>(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<string, fieldAtom>
emptyValue = {} as Value;
emptyValue = {} as Values;
}

function buildFields(): Fields;
function buildFields(data: ListFieldValue<Fields>[]): Fields[];
function buildFields(data?: ListFieldValue<Fields>[]) {
/**
* @FIXME
* HACK2:
* the data is not simply Values, as that would produce the any[] on the returned function.
*/
function buildFields(data: ListFieldValues<Fields>[]): Fields[];
function buildFields(data?: Values[]) {
if (data) {
return data.map(builder);
} else {
Expand Down

0 comments on commit 8beff9b

Please sign in to comment.