Skip to content

Commit

Permalink
Merge pull request #8 from atmina/feat/discriminate
Browse files Browse the repository at this point in the history
feat: $discriminate for union types
  • Loading branch information
reiv authored Feb 19, 2024
2 parents f2a7c1f + 1f6ae38 commit 8c821b1
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 3 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ wrap the primitive in an object, or use a controller (`$useController`) to imple

For more information, see the React Hook Form docs on [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray).

## Discriminating unions

In case of a form that contains fields with object unions, the `$discriminate()` function may be used to narrow the type
using a specific member like this:

```tsx
import { FC } from 'react';

type DiscriminatedForm =
| { __typename: 'foo'; foo: string; }
| { __typename: 'bar'; bar: number; }

const DiscriminatedSubform: FC<{field: FormBuilder<DiscriminatedForm>}> = ({field}) => {
const fooForm = field.$discriminate('__typename', 'foo');

return <input {...fooForm.foo()} />;
};
```

> [!IMPORTANT]
> `$discriminate` currently does **not** perform any runtime checks, it's strictly used for type narrowing at this time.

## Compatibility with `useForm`

Currently, `useFormBuilder` is almost compatible with `useForm`. This means you get the entire bag of tools provided by
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atmina/formbuilder",
"version": "1.0.0",
"version": "1.1.0",
"description": "A strongly-typed alternative API for React Hook Form.",
"source": "src/index.ts",
"main": "lib/index.js",
Expand Down Expand Up @@ -73,5 +73,6 @@
"react-dom": {
"optional": true
}
}
},
"packageManager": "[email protected]+sha256.dbed5b7e10c552ba0e1a545c948d5473bc6c5a28ce22a8fd27e493e3e5eb6370"
}
13 changes: 12 additions & 1 deletion src/formbuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export type FormBuilder<T> = FormBuilderRegisterFn<T> & {
options: Parameters<UseFormSetError<FieldValues>>[2]
): void;
$setFocus(options: SetFocusOptions): void;
$discriminate<TKey extends keyof T, TValue extends T[TKey]>(
key: TKey,
value: TValue
): IsUnknown<T> extends 1
? FormBuilder<unknown>
: FormBuilder<Extract<T, Record<TKey, TValue>>>;
} & (T extends Primitive
? // Leaf node
unknown
Expand Down Expand Up @@ -152,7 +158,7 @@ export function createFormBuilder<TFieldValues extends FieldValues>(
return methods.register(currentPath, options as never);
}) as FormBuilder<TFieldValues>,
{
get(_, prop) {
get(_, prop, receiver) {
let useCached = cache[prop];
if (useCached !== undefined) {
return useCached;
Expand Down Expand Up @@ -225,6 +231,9 @@ export function createFormBuilder<TFieldValues extends FieldValues>(
methods.setError(currentPath, value, options);
};
break;
case "$discriminate":
useCached = () => receiver;
break;
default:
// Recurse
useCached = createFormBuilder<TFieldValues>(methods, [
Expand Down Expand Up @@ -353,3 +362,5 @@ export type UseFormBuilderProps<
TFieldValues extends FieldValues = FieldValues,
TContext = any
> = UseFormProps<TFieldValues, TContext>;

type IsUnknown<T> = unknown extends T ? (T extends unknown ? 1 : 0) : 0;
19 changes: 19 additions & 0 deletions src/types.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ describe("Types", () => {
FormBuilder<{ things: { foo: string }[]; otherThings: { bar: string }[] }>
>().toMatchTypeOf<FormBuilder<{ things: { foo: string }[] }>>();

// discriminate helper
interface FooForm {
__typename: "foo";
foo: string;
}
interface BarForm {
__typename: "bar";
bar: number;
}
const discriminatorForm: FormBuilder<FooForm | BarForm> = {
$discriminate: () => undefined,
} as any;
expectTypeOf(
discriminatorForm.$discriminate("__typename", "foo")
).toMatchTypeOf<FormBuilder<{ foo: string }>>();
expectTypeOf(
discriminatorForm.$discriminate("__typename", "bar")
).toMatchTypeOf<FormBuilder<{ bar: number }>>();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
expectTypeOf<FormBuilder<{ foo: string }>>().toMatchTypeOf<
FormBuilder<any>
Expand Down

0 comments on commit 8c821b1

Please sign in to comment.