svelte-forms-lib icon indicating copy to clipboard operation
svelte-forms-lib copied to clipboard

Form array doesn't work with Typescript

Open mpicciolli opened this issue 2 years ago • 2 comments

Summary

Steps to reproduce

Copy paste the form array code example here and change the script lang for ts

Example Project

<script lang="ts">
    import { createForm } from 'svelte-forms-lib';
    import * as yup from 'yup';

    const { form, errors, state, handleChange, handleSubmit, handleReset } = createForm({
        initialValues: {
            users: [
                {
                    name: '',
                    email: '',
                },
            ],
        },
        validationSchema: yup.object().shape({
            users: yup.array().of(
                yup.object().shape({
                    name: yup.string().required(),
                    email: yup.string().email().required(),
                })
            ),
        }),
        onSubmit: (values) => {
            alert(JSON.stringify(values));
        },
    });

    const add = () => {
        $form.users = $form.users.concat({ name: '', email: '' });
        $errors.users = $errors.users.concat({ name: '', email: '' });
    };

    const remove = (i) => () => {
        $form.users = $form.users.filter((u, j) => j !== i);
        $errors.users = $errors.users.filter((u, j) => j !== i);
    };
</script>

<form>
    <h1>Add users</h1>

    {#each $form.users as user, j}
        <div class="form-group">
            <div>
                <input
                    name={`users[${j}].name`}
                    placeholder="name"
                    on:change={handleChange}
                    on:blur={handleChange}
                    bind:value={$form.users[j].name}
                />
                {#if $errors.users[j].name}
                    <small class="error">{$errors.users[j].name}</small>
                {/if}
            </div>

            <div>
                <input
                    placeholder="email"
                    name={`users[${j}].email`}
                    on:change={handleChange}
                    on:blur={handleChange}
                    bind:value={$form.users[j].email}
                />
                {#if $errors.users[j].email}
                    <small class="error">{$errors.users[j].email}</small>
                {/if}
            </div>

            {#if j === $form.users.length - 1}
                <button type="button" on:click={add}>+</button>
            {/if}
            {#if $form.users.length !== 1}
                <button type="button" on:click={remove(j)}>-</button>
            {/if}
        </div>
    {/each}

    <div class="button-group">
        <button type="button" on:click={handleSubmit}>submit</button>
        <button type="button" on:click={handleReset}>reset</button>
    </div>
</form>

<style>
    .error {
        display: block;
        color: red;
    }
    .form-group {
        display: flex;
        align-items: baseline;
    }
    .button-group {
        display: flex;
    }
    button ~ button {
        margin-left: 15px;
    }
</style>

CodeSandox

What is the current bug behavior?

There are some typescript errors :

  • Argument of type '{ name: string; email: string; }' is not assignable to parameter of type 'string'.
  • Property 'name' does not exist on type 'string'

What is the expected correct behavior?

No typescript error anymore

Relevant logs and/or screenshots

image

image

mpicciolli avatar Jan 10 '22 22:01 mpicciolli

Same experience on using a yup.array(yup.string()).

Currently my workaround is to explicitly cast:

$: fruitErrors = $errors.fruits as unknown as string[];

Looks like this comes from $errors as being a record whose values are always string:

https://github.com/tjinauyeung/svelte-forms-lib/blob/master/lib/index.d.ts#L34

Should errors be Writable<Inf> directly, so its values are supposed to have the same shape as the form values? Looks like this is what svelte-forms-lib actually exposes. At least when using yup -- this seems to be specific:

https://github.com/tjinauyeung/svelte-forms-lib/blob/7df1b1df4f02c5ff512e2542c879056ac2d8138d/lib/create-form.js#L142-L164

I'm not TypeScript definitions guru, but I assume it would be possible to refine createForm and FormState declarations to accept a generic parameter TShape extends yup.ObjectShape which would be plugged into validationSchema: ObjectSchema<TShape> and into errors: Writable<TShape>. Currently validationSchema is marked as optional but it seems to be the condition between a generic form and a yup-powered form.

florimondmanca avatar Apr 04 '22 09:04 florimondmanca

Another solution to this is to cast is before assigning it to <Form>.

import type { FormProps } from 'svelte-forms-lib';
import { Form, Field } from 'svelte-forms-lib';

// Specify your own form field type, to you have safe typing
type FormFields = {
  email: string
  username: string
  password: string
}

const formProps: FormProps<FormFields> = {
    initialValues: {}
    .... etc
}

// Cast formProps to the type <Form> expects
const props = formProps as FormProps; // ← ← ←

Then assign props instead of formProps.

<Form {...props}>
    <!-- form contents -->
</Form>

But this behavior does seem like a bug.

ecker00 avatar Jun 25 '22 20:06 ecker00