jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
34k stars 2.79k forks source link

Make <FieldArray /> & FieldArrayRenderProps type-safer #3992

Open Gnomeek opened 2 months ago

Gnomeek commented 2 months ago

Feature request

Hi team, I'd like to propose an improvement on <FieldArray /> or FieldArrayRenderProps to make it type-safer.

Current Behavior

Currently, we only have generic type on ArrayHelpers https://github.com/jaredpalmer/formik/blob/2618cc4e6af0b2b1fcbff93936b5d3c68809b791/packages/formik/src/FieldArray.tsx#L20-L31

Desired Behavior

T of ArrayHelpers<T> should be inferred from FieldArrayRenderProps.

Suggested Solution

Maybe we can leverage knowledge from Get from type-fest to achieve something like:

import type {Get, Paths} from 'type-fest';
import { LiteralStringUnion } from 'type-fest/source/literal-union';
import { ToString } from 'type-fest/source/internal';

type Path<T> = LiteralStringUnion<ToString<Paths<T, {bracketNotation: false}> | Paths<T, {bracketNotation: true}>>>

export type FieldArrayRenderProps<
  Values = FormikValues,
  Name extends Path<Values> = any> = 
ArrayHelpers<Get<Values, Name> extends any[] ? Get<Values, Name> : any> & {
  form: FormikProps<Values>;
  name: Name;
};

export type FieldArrayConfig<Values = FormikValues, Name extends Path<Values> = any> = {
  /** Really the path to the array field to be updated */
  name: Name;
  /** Should field array validate the form AFTER array updates/changes? */
  validateOnChange?: boolean;
} & SharedRenderProps<FieldArrayRenderProps<Values, Name>>;
export interface ArrayHelpers<T extends any[] = any[]> {

Who does this impact? Who is this for?

A type-safer <FieldArray /> or FieldArrayRenderProps can improve the type coverage of the package and avoid lots of unsafe cases.

Corner cases

For nested object

type Values = {
  friends: string[];
  friendsObj: {
    name: string;
    friends: string[];
    friendsObj: {
      name: string;
    }[];
  }[];
};

type case3 = ArrayHelpers<Get<Values, 'friendsObj[0].friendsObj'>>
// will be ArrayHelpers<any> since `friendsObj[0].friendsObj` can be undefined.
// allow ArrayHelpers<T extends (any[] | undefined) = any[]> can be problematic
Gnomeek commented 2 months ago

Another idea comes to my mind. We can implement a ExtractArrayTypes<T> which extracts all types(include nested) in T that extends Array, thus:

export type FieldArrayRenderProps<
  Values extends FormikValues,
  T extends ExtractArrayTypes<Values>
> = ArrayHelpers<T> & {
  form: FormikProps<Values>;
  name: string;
};