Angular Reactive Forms - Some Type Safety~
Tags: angular, typescript, webdev
Published: 2020-02-04 09:10:00

Reactive forms in angular are a very powerful way to create forms. Unfortunately, the form builder does not support any type safety.
Small typos can go unnoticed and using models for multiple forms can be frustrating to maintain.

But thanks to TypeScript there are some ways to overcome those issues :).

## The problem
```typescript
interface Person {
  firstname: string;
  lastname: string;
}

export class AppComponent {
  form: FormGroup;
  
  constructor(fb: FormBuilder) {
    this.form = fb.group({
      firstName: [null],
      lastNamee: [null]
    });
  }
}
```
By creating a form the default way, some general issues are not covered. The form does not match the defined model. At the latest when using your form value with an API or using the `formControlName` directive you might notice the error. 

## Using Record
`Record<K, P>` is a utility type provided by TypeScript. It requires two type arguments ([documentation](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt)).

```typescript
interface Person {
  firstname: string;
  lastname: string;
}

export class AppComponent {
  form: FormGroup;
  
  constructor(fb: FormBuilder) {
    const form: Record<keyof Person, any> = {
      firstname: [null],
      lastname: [null]
    };
    this.form = fb.group(form);
  }
}
```

## Using a custom type
If you'd like to avoid repeating the `Record<keyof T, any>` syntax, you can declare a `type` for general uses. This custom type can also be declared more specific. But for now, we go with the `any` type to accomplish the same result as before.

```typescript
type FormGroupModel<T> = {
  [x in keyof T]: any;
}

interface Person {
  firstname: string;
  lastname: string;
}

export class AppComponent {
  form: FormGroup;
  
  constructor(fb: FormBuilder) {
    const form: FormGroupModel<Person> = {
      firstname: [null],
      lastname: [null]
    };
    this.form = fb.group(form);
  }
}
```

## Dive deeper
To make it more type safe, like passing the correct value or validators, we can enhance the `FormGroupModel` a little bit more.

```typescript
type ValidatorModel<T, K> = T | K | (T | K)[];

type FormGroupModel<T> = {
  [x in keyof T]: [
    T[x],
    ValidatorModel<Validator, ValidatorFn>?,
    ValidatorModel<AsyncValidator, AsyncValidatorFn>?
  ];
};

```
Changing the `any` type to this ominous code makes it more restricted. The type now accepts an array with up to 3 arguments. 

The first argument in the array (`T[x]`) states, that the initial value has to match with the defined model.

```typescript
interface Person {
  firstname: string;
  lastname: string;
}

const form: FormGroupModel<Person> = {
  firstname: ['Randy'], // valid
  lastname: [123]       // invalid
};
```

The second argument can be a Validator or a ValidatorFn or a set of Validator and ValidatorFn.

```typescript
const form: FormGroupModel<Person> = {
  firstname: ['Randy', Validators.required],
  lastname: ['McRandom', [Validators.required, Validators.minLength(5)]]
};
```

The third argument works like the second, but for AsyncValidator and AsyncValidatorFn.


## Summary
With those approaches, the form is enforced to be built correctly. The biggest benefit is, that when the model changes, the compiler will complain that the form does not match. This will make your project way more maintainable 👍.