Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strongly Typed Formik, v3 #3152

Draft
wants to merge 113 commits into
base: milestone/v3
Choose a base branch
from

Conversation

johnrom
Copy link
Collaborator

@johnrom johnrom commented Apr 18, 2021

You can test these changes (and v3 in general) out in development by hot swapping formik for @johnrom/formik-v3@3.1.0-types9 (or whatever the latest version is here). NOT for production use!

Adds strong types to Formik by typing Fields with Field<Value, FormValues>. This is an extremely breaking change. You can view only the changes related to this PR here: THESE CHANGES

Opting-in to Types

  • createTypedField<YourFormValuesType>() (anywhere)
  • useTypedField<YourFormValuesType>() (in component) (ok they're the same but you'll feel safer if you use a hook in your component, plus we could optimize later)
  • <Field<number, YourFormValuesType> />
  • A custom field

Using *TypedField()

Using useTypedField or createTypedField above, you can let the component know which Values object to look through so you can get automatic inference when passing a component or path to your field, like:

const TypedField = createTypedField<{num: number, str: string}>();

const test1 = () => <TypedField name="num" validate={(num) => { /* num is a number */ }} />
// the following will autosuggest only paths which match a number value, like "num" above
const test2 = () => <TypedField as={(props: FieldAsProps<number, Values>) => {}} name="" /> 

A Custom Field

Alternatively, you can create a custom field which accepts FieldConfig<number, Values>, like this. I find this to be the easiest to use and most flexible.

const MyNumberField = <Values,>(props: FieldConfig<number, Values>) => 
  <Field {...props} 
    validate={validateNumber}
    parse={parseNumber}
    format={formatNumber}
  />;

// this will only suggest `num`
const test = () => <MyNumberField<{ num: number, str: string}> name="" />

Use both!

MyNumberField can call <Field as={MyNumberAsComponent} /> It's a little bit confusing from there, but can be useful if you use your custom field as a "connector" between Formik and your raw input component.

Examples

Check out every possible variant here: https://codesandbox.io/s/serene-napier-4wjxs?file=/src/App.tsx

How does it work

This PR separates the different types of possible Field configs into separate types, then tries to match name based on the Value configured for a given Field. It also allows the Value to be inferred from the name if FormValues is known and there are no explicit usages of Value. The different Field configurations are:

// FieldDefaultConfig, infers Value from name when using TypedField
const fieldDefaultConfig = () => <Field name="" />
// FieldAsStringConfig, infers Value from name when using TypedField
const fieldAsStringConfig = () => <Field as="textarea" name="" />
// FieldAsComponentConfig, infers Value from AsComponent
const fieldAsComponentConfig = () => <Field as={Component} name="" />
// FieldComponentStringConfig, infers Value from name when using TypedField
const fieldComponentStringConfig = () => <Field component="textarea" name="" />
// FieldComponentComponentConfig, infers Value from ComponentComponent (lol)
const fieldComponentComponentConfig = () => <Field component={Component} name="" />
// FieldChildrenConfig, infers Value from FieldRenderProps, if typed, or name if inferred
const fieldChildrenConfig = () => <Field>
  {(props: FieldRenderProps) => null}
</Field>
// FieldRenderConfig, infers Value from FieldRenderProps, if typed, or name if inferred
const fieldRenderConfig = () => <Field render={(props: FieldRenderProps) => null} />

Todo

  • FieldArray
  • Type "AnyFieldName" with a flat list of possible field names.
  • Switch to Interface inheritance over intersections where possible (esp FieldConfig types) to improve TS performance when inferring FieldConfig variants. docs. It might also help us infer ExtraProps.
    • TL;DR: type oneOf = interface IA extends A, B, C {} | interface IB extends A, B, D {} is fast, but
    • type oneOf = type (A & B & C) | type (A & B & D) is quite slow.
  • Doesn't seem like PathMatchingValue matches extending values, like PathMatchingValue<number, Values> is not extensible to paths matching number | "".
    • setFieldValue<number>(field: PathMatchingValue<number | "">, value: number) cannot be inferred, must manually call setFieldValue<number | "">.
  • Support textarea props, like rows. Previously this would be inferred by ExtraProps but as="textarea" doesn't have extra props.
  • Support ExtraProps
    <Field myExtraProp={true} as={MyComponentExpectingMyExtraProp} />
  • Expand strong types to the rest of Formik

The following can be worked around by simply using untyped variants of these complex fields

  • Add subfield for FieldArray to connect ${props.path}.${index} with ValueMatchingPath<number, ValueMatchingPath<Path, Values>> which is a very complicated way of saying (infer Value)[].
  • Add support for multi-checkboxes
  • Check whether select multiple works.

johnrom and others added 30 commits March 8, 2021 18:28
Added Ref State.
Added useSelectorComparer.
Starting to build subscriptions.
use-subscriptions might not work in React 17? Seems returning the previous value doesn't bail out the render.
Sync up formik-native and formik for v3.
…te will do. If we call our own reducer then use useReducer dispatch, `state.values !== getState().values`.
…ormikReducerState + FormikComputedState.

Add Fixtures and Tutorial code to /app.
Consolidate State and Add Tutorial + Fixtures.
For some reason, the tests for remove only worked when they were async or render was moved from beforeEach to the individual tests.
@johnrom
Copy link
Collaborator Author

johnrom commented May 14, 2021

Published @johnrom/formik-v3@3.1.0-types9, adding PathLikeValue alongside PathMatchingValue for functions like setFieldValue where the value might be a narrower type and the path could match a wider type, like setFieldValue("numberOrString", numValue);. There's still an issue specifically with constant strings, like number | "" that will require casting due to microsoft/TypeScript#30808

johnrom and others added 11 commits May 16, 2021 12:51
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
setError is setFieldError bound to specific field. Therefore the setError value should have same type as setFieldError message
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
# Conflicts:
#	packages/formik/src/Formik.tsx
#	packages/formik/src/types.tsx
@github-actions
Copy link
Contributor

This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

@github-actions github-actions bot added the stale label Jun 17, 2021
@johnrom johnrom removed the stale label Jun 17, 2021
@github-actions
Copy link
Contributor

This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

@github-actions github-actions bot added the stale label Jul 18, 2021
@johnrom johnrom removed the stale label Jul 18, 2021
…types-v3

# Conflicts:
#	app/package.json
#	packages/formik-native/package.json
#	packages/formik/package.json
#	packages/formik/src/Formik.tsx
#	packages/formik/src/hooks/hooks.ts
#	packages/formik/src/types.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants