How to Forms with React and Typescript
Tags: functional, react, typescript, forms
Published: 2020-01-15 21:50:30

Forms are hard because they tend to be complex. Email validation is easy, but configurable form with several sync and async validations is hard. That's why Facebook created React by the way. To handle all that logic somehow. And yet still after many years, here I am writing yet another article about form validation. But not only about it, because validation is just one piece from the bigger picture.

# Domain Model

Everything starts with data and their shape - a domain model. A form is a domain model too. Is this ok or not?

```ts
interface SignUpForm {
  email: string;
  password: string;
}
const signUpForm: SignUpForm = { email: '', password: '' };
```

Is any string email? No. A string must be validated to be an email. The same for a password. SignUpForm with strings is suboptimal. In classical Java etc. world, we would have to use classes. With TypeScript, we can use branded types.

```ts
import * as t from 'io-ts';

interface EmailBrand {
  readonly Email: unique symbol;
}

// This will create runtime type.
const Email = t.brand(
  t.string,
  // isEmail from validator.
  (s): s is t.Branded<string, EmailBrand> => isEmail(s),
  'Email',
);
// This will create TypeScript type from io-ts runtime type.
type Email = t.TypeOf<typeof Email>

interface SignUpForm {
  email: Email;
  password: Password;
}

// No, we can't just assign a string. TypeScript compiler will tell us.
const signUpForm: SignUpForm = { email: '', password: '' };

// Email has a decode method, which return either Error or Email.
const either: Either<Error, Email> = Email.decode(whatEver);

```

It seems to be a lot of code, but once it's written, it's reusable forever. We created some basic runtime types for you. Functional programming is all about composition. Sure we can compose runtime types too. Check a full-fledged example of a sign-up form.


```ts
import { String64, Email, UniqueEmail, Password, Phone } from 'typescript-fun';

const SignUpForm = t.type({
  company: String64,
  email: t.union([Email, UniqueEmail]),
  password: Password,
  phone: option(Phone),
  sendNewsletter: t.boolean,
});
```

There are some interesting things. The email field is a union of Email and UniqueEmail. Every UniqueEmail is Email but not every Email is UniqueEmail. Take a look at User type. We can not pass any Email there, only UniqueEmail. The typeScript compiler ensures it.

```ts
import { String64, Email, UniqueEmail, Password, Phone } from 'typescript-fun';

const User = t.type({
  email: UniqueEmail,
});
```

Email type itself is the intersection of String64 and EmailString. And String64 is the intersection of NonEmptyTrimmedString and Max64String. 

Note `option(Phone)`, the option is a generic type. It makes any type optional. Traditionally, we would use a null or undefined or empty string, which is a very suboptimal and not generic approach. We need to tell "This type is an optional type of another type.". We use the [Option](https://gcanti.github.io/fp-ts/modules/Option.ts.html) monad.   

Every branded type has a string name, which we can use for validation error messages. If some email is Email, we can check whether it's also unique on the server, and if not, we can show error "This email is already used." The beauty of this approach is endless scalability and perfect type correctness from day 0.

# Forms

So far we have seen a domain model that can be validated in any context, but how to make a form from that? We made a little yet powerful React Hook for that.

```ts
const form = useForm(
  SignUpForm,
  {
    company: '',
    email: '',
    password: '',
    // O.none, because it's explicit. Null, undefined, empty string are not.
    phone: O.none,
    sendNewsletter: false,
  },
  {
    handleSubmit(form) {
      pipe(
        form.validated,
        E.fold(constVoid, data => {
          form.disable();
          // Simulate async sign up.
          setTimeout(() => {
            form.enable();
            if (data.email === 'a@a.com') {
              form.setAsyncErrors({ email: ['UniqueEmail'] });
            } else {
              alert(JSON.stringify(data, null, 2));
              form.reset();
            }
          }, 1000);
        }),
      );
    },
  },
);

```

React Hook useForm provides a lot of helpers for any forms written with any UI library. Check [typescript.fun/examples](https://typescript.fun/examples).

We plan to write more articles about typed functional programming in TypeScript and more helpers like useForm.

Follow us [twitter.com/estejs](https://twitter.com/estejs) or check our [typescript.fun](https://typescript.fun/) web.