Skip to content

withControl typing bug if controlFactory props optional #18

Open

Description

@kyeotic

I have a control defined using withControl and the expected props for the control and the component are reversed.

Screenshots

  • Bad control typing
    Image

  • Bad component typing
    Image

Code

import { For } from 'solid-js'
import {
  createFormGroup,
  createFormControl,
  withControl,
  createFormArray,
  bindOwner,
} from 'solid-forms'
import { ItemWithNote } from '../../../server/users/types'
import { H2, Button } from '../../components'
import { TextInput } from '../../components/Forms'

const newItem = (item?: ItemWithNote) =>
  createFormGroup({
    item: createFormControl(item?.item ?? ''),
    note: createFormControl(item?.note ?? ''),
  })

const controlFactory = (props: { initialValue: ItemWithNote[] }) => {
  return createFormArray<ReturnType<typeof newItem>[]>(
    props.initialValue.map(newItem),
  )
}

export const ItemWithNoteSet = withControl<
  { title: string },
  typeof controlFactory
>({
  controlFactory,
  component: (props) => {
    const array = () => props.control

    const onAdd = bindOwner(() => array().push(newItem()))

    const onDelete = (index: number) => {
      array().removeControl(index)
    }

    return (
      <div>
        <H2 class="mb-2">{props.title}</H2>
        <Button onclick={onAdd}>Add</Button>
        <For each={array().controls}>
          {(control, index) => (
            <div class="p-4 border rounded mt-2 space-y-2">
              <div class="flex justify-between items-start">
                <TextInput label="Item" control={control.controls.item} />
                <Button danger onclick={() => onDelete(index())}>
                  Delete
                </Button>
              </div>
              <TextInput label="Note" control={control.controls.note} />
            </div>
          )}
        </For>
      </div>
    )
  },
})

Activity

  1. jorroll commented on Jun 6, 2025

    @jorroll
    Owner

    The first example ("bad control typing") looks like it's behaving as expected. It looks like you've typed the controlFactory function as requiring a props argument with the interface { initialValue: ItemWithNote[] }. Later when you're calling that controlFunction (via ItemWithNoteSet.control()) you're getting a type error because you haven't provided the required props: { initialValue: ItemWithNote[] } argument, which is working as I'd expect (if the props / initialValue are optional, it should be typed that way).

    The second example does look like a typing issue with this library though. You should be able to provided either a pre-constructed control prop or an initialValue prop. Is this the error you are referring to? I haven't actually used the withControl helper in quite a while (I've ended up deciding that it I prefer not using it).

    I imagine you can fix the first issue and also work around the second by typing the controlFactory props as optional.

    const controlFactory = (props?: { initialValue?: ItemWithNote[] }) => {
      return createFormArray<ReturnType<typeof newItem>[]>(
        props?.initialValue?.map(newItem) ?? [],
      )
    }
  2. kyeotic commented on Jun 6, 2025

    @kyeotic
    Author

    🤦 You're right about the first one, I saw props and I was thinking "oh component props". Providing the value to it silences that error. It doesn't silence the Component error though. Making it optional silence both, but that doesn't feel right though. I'm sure I am going to have a case where there are non-optional control props, and when I do its going to throw this typing error.

    I've ended up deciding that it I prefer not using it

    What is your preferred alternative?

  3. kyeotic commented on Jun 6, 2025

    @kyeotic
    Author

    Actually, it still fails if I re-write the controlFactory to take the initial value directly, like the normal createFormControl does

    const controlFactory = (initialValue?: ItemWithNote[]) => {
      return createFormArray<ReturnType<typeof newItem>[]>(
        initialValue?.map(newItem) ?? [],
      )
    }
    // component
    
    export const ItemWithNoteSet = withControl<
      { title: string },
      typeof controlFactory
    >({
      controlFactory,
      component: (props: {
        title: string
        control: ReturnType<typeof controlFactory>
      })
    

    Usage

    neurotypes: ItemWithNoteSet.control(props.profile?.neurotypes)

    Component Usage (errors)

    <ItemWithNoteSet
                title="Neurotypes"
                control={form.controls.neurotypes}
              />

    Error

    Type '{ title: string; control: IFormArray<IFormGroup<{ item: IFormControl<string, Record<ControlId, any>>; note: IFormControl<string, Record<ControlId, any>>; }, Record<...>>[], Record<...>>; }' is not assignable to type 'IntrinsicAttributes & { title: string; } & { item: string; note?: string | undefined; }[] & { children?: Element; } & { control?: IFormArray<IFormGroup<{ item: IFormControl<string, Record<ControlId, any>>; note: IFormControl<...>; }, Record<...>>[], Record<...>> | undefined; }'.
      Type '{ title: string; control: IFormArray<IFormGroup<{ item: IFormControl<string, Record<ControlId, any>>; note: IFormControl<string, Record<ControlId, any>>; }, Record<...>>[], Record<...>>; }' is missing the following properties from type '{ item: string; note?: string | undefined; }[]': length, pop, push, concat, and 35 more.ts(2322)
    
  4. jorroll commented on Jun 6, 2025

    @jorroll
    Owner

    Making it optional silence both, but that doesn't feel right though. I'm sure I am going to have a case where there are non-optional control props, and when I do its going to throw this typing error.

    Agreed, I don't think the current behavior is correct in this case. But hopefully this approach will allow you to quickly move forward. Ideally the withControl typing would be improved though.

    Actually, it still fails if I re-write the controlFactory to take the initial value directly, like the normal createFormControl does

    Like a component, the controlFactory prop must be POJO object.

    I.e. this works

    const controlFactory = (props?: { initialValue?: ItemWithNote[] }) => {
      return createFormArray<ReturnType<typeof newItem>[]>(
        props?.initialValue?.map(newItem) ?? [],
      )
    }

    But this does not

    const controlFactory = (initialValue?: ItemWithNote[]) => {
      return createFormArray<ReturnType<typeof newItem>[]>(
        initialValue?.map(newItem) ?? [],
      )
    }

    Then usage would be like

    ItemWithNoteSet.control({ initialValue: props.profile?.neurotypes });

    I've ended up deciding that it I prefer not using it

    What is your preferred alternative?

    I've found that I prefer always just creating the control in the parent and passing it in via a control prop. the withControl helper function basically facilitates creating a form component that doesn't need the parent to pass in a control, but in practice I've never actually wanted that feature and using withControl adds a fair bit of complexity (without practical value, in my experience).

    • Edit: In practice, I think withControl might be useful if you were creating a component library built on top of solid-forms. In this case, I could imagine wanting to create form input components which internally use FormControl objects to manage state but which shouldn't require the user to pass in a control to use them. E.g. maybe the user just passes in callback props like onChange to use the control. In this case, withControl would add some complexity to the creator of the library but would reduce the complexity for the consumer of the library. But if you're making the input components for personal use, I think it's easier to just not use withControl and instead pass in a control object from parent to child.
  5. changed the title [-]withControl props are backwards[/-] [+]withControl typing issue if controlFactory props optional[/+] on Jun 6, 2025
  6. changed the title [-]withControl typing issue if controlFactory props optional[/-] [+]withControl typing bug if controlFactory props optional[/+] on Jun 6, 2025
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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions