Skip to content

[RFC] form-core: Handling Array Methods Called on null or undefined #1823

Description

@LeCarbonator

RFC (form-core): Handling Array Methods Called on null or undefined

Background

TanStack Form provides several helper methods for working with array fields.
In version [email protected], these include:

  • moveFieldValues
  • pushFieldValue
  • swapFieldValues
  • clearFieldValues
  • insertFieldValue
  • removeFieldValue
  • replaceFieldValue

Currently, these methods can only be used if the field type explicitly extends unknown[]. However, there have been requests (#1588 ) to support optional arrays.

The Problem

TypeScript allows us to express such optional arrays easily, but at runtime the behavior is inconsistent.
Consider this example:

const form = useForm({
  defaultValues: {
    people: null as null | string[]
  }
})

form.pushFieldValue('people', 'New person')

In the current version:

  • Some methods error,
  • Some methods don't do anything,
  • Some methods silently create a new array

There are no runtime checks ensuring consistent behavior when array methods are called on null or undefined

Goal

We want consistent, predictable behavior across all array helper methods when the field value is null or undefined.

This RFC plans to define that behaviour and make it guarantee the runtime behaviour.
Once a decision is made, all array methods (swapFieldValues, removeFieldValue etc.) will follow this rule and be tested for it.

Your Feedback

If you currently use arrays in your forms, please share your expectations for how the following code should behave:

form.pushFieldValue('people', 'New person')

Here are some of our proposed options. If you have other suggestions or proposals, feel free to share!

  1. Throw an error: The operation should explicitly fail if it encountered null or undefined.
  2. Create an empty array: The operation should first initialize an array, and then apply the changes if possible.
    • pushFieldValue('people', 'New Person') transforms null -> ['New Person']
    • swapFieldValues('people', 1, 2) transforms null -> []
  3. Do nothing: The operation should not apply any changes if the field is not an array.

Frequently Asked Questions

Will this be a breaking change?

Possibly. The current type definitions don't allow you to call these methods on optional arrays. If a field called the array method, it was an inconsistent mix of doing nothing, throwing an error or creating an array.
With the plans to provide optional array support, we want to enforce consistent behaviour.

Activity

  1. changed the title [-]-[/-] [+][RFC] form-core: Handling Array Methods Called on `null` or `undefined`[/+] on Oct 24, 2025
  2. pinned this issue on Oct 24, 2025
  3. alexisdorosz commented on Oct 24, 2025

    @alexisdorosz

    We use GO for our backend and usually use backend response as our default values for forms.
    Since GO slices are pointers with a zero value of nil, we treat all our arrays as nullable() for safety.

    It is also quite common in UI library to have null represent the default value for Select or Combobox, some of which have a "multiple" mode allowing to pick multiple choices.

    For users of such components, I assume it would be convenient for an array field to start as null or undefined.

    We believe Option 2. Create an empty array upon using an array method would be the most convenient behaviour for these scenarios. 😊

  4. rob-steele-active commented on Apr 21, 2026

    @rob-steele-active

    Happened upon this issue so I figured I'd give my two cents.

    I tend to prefer explicit typing and so I don't really see the benefit of allowing null/undefined and automatically creating an empty array. Checking if an array has zero length is about the same amount of code in javascript as checking if something is null.

    I like to think of the form's type as it's interface. It shouldn't matter how it will be saved later, the form has a certain shape and the story is told in that shape. For me, an array that might not have anything in it should still be an array.

    As for other language interfaces like Go for example I'm not sure what the benefit would be there but I don't know enough about that use case to refute any potential benefits as my day to day is in javascript.

    My vote would be option 1, throw an error if null or undefined is found. Avoiding hidden coercion is more valuable to me than convenience generally and I don't see how the coercion provides much convenience to begin with.

  5. 2yunseong commented on Jul 29, 2026

    @2yunseong

    I’d vote for Option 1 (Throw an Error) as well.

    To me, null or undefined doesn’t mean quite the same thing as []. A nullable value might mean the field hasn’t been initialized or doesn’t apply, while an empty array means it’s initialized but has no items.

    Throwing an error makes unexpected form values obvious and avoids hiding problems with the initial values or the API mapping.

    The array helpers shouldn’t have to make that decision on the consumer’s behalf.

  6. added
    v1This issue has been created during v1 development.
    v2This issue affects v2 as well.
    scope: coreThis issue affects the core package, meaning any adapter is also affected by it.
    on Aug 9, 2026
  7. IAluI commented on Sep 26, 2026

    @IAluI

    Our vote goes to option 2 ("Create an empty array") — with one caveat regarding empty results (see the "Arrays" section).

    Context

    We generate code from OpenAPI using hey-api and zod-empty (a customized fork).

    Strings

    For the OpenAPI schema

    Foo:
        type: object
        properties:
            bar:
                type: string

    hey-api generates the Zod schema

    const schema = z.object({
        bar: z.string().optional(),
    });

    and zod-empty derives the form's initial state from the Zod schema:

    const defaultValues = {
        bar: undefined,
    };

    The schema above corresponds to the type

    type DefaultValues = {
        bar?: string;
    };

    Here it is clearly visible that the field is optional. To use an empty string as the initial state for the required bar, we would have to switch to the OpenApi scheme

    Foo:
        type: object
        properties:
            bar:
                type: string
                minLength: 1

    Instead of

    Foo:
        type: object
        properties:
            bar:
                type: string
        required: [bar]

    This looks less idiomatic and, worst of all, is not reflected in the static type.

    Having two semantically equivalent states — '' and undefined — is a bad idea. That is why it is better if '' is disallowed and the model transitions to the undefined state instead. This differs from the TanStack Form docs examples, where an empty string is often used for string fields, but this approach is more idiomatic from a TypeScript and OpenAPI perspective. That said, the React warning

    A component is changing an uncontrolled input to be controlled...

    can be avoided as follows:

    <input
        value={field.state.value ?? ''}
        onChange={(e) => field.handleChange(e.target.value ? e.target.value : undefined)}
    />

    Usually we build forms to send data over the network, and here undefined also has advantages:

    JSON.stringify({ bar: undefined }) // -> {}
    JSON.stringify({ bar: '' })        // -> {"bar":""}

    Arrays

    The same logic applies to arrays, which is why I consider "Create an empty array" the best option.

    Additionally, I propose that any operation resulting in an empty array should produce undefined instead of []. In particular, removeFieldValue would set the value to undefined rather than [] when the last element is removed.

    Such behavior aligns better with TypeScript and OpenAPI. It would let us leverage code generation more broadly and eliminate a lot of boilerplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: coreThis issue affects the core package, meaning any adapter is also affected by it.v1This issue has been created during v1 development.v2This issue affects v2 as well.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions