As of Angular v14, reactive forms are strictly typed. Type safety in the forms was missing for years and it is a huge step forward in making sure that our forms are going to work correctly. The type of the form can be now explicitly defined as following:

interface UserForm {
    email: FormControl<string>;
    name: FormControl<string>;
}

const userForm = new FormGroup<UserForm>({
    email: new FormControl<string>(''),
    name: new FormControl<string>(''),
});

Pretty cool, right?

There is one improvement I could use though, and that is defining the type of the forms by passing a type of the form’s value rather than the type of the FormGroup itself.

So, instead of doing this:

interface UserForm {
    email: FormControl<string>;
    name: FormControl<string>;
}

const userForm = new FormGroup<UserForm>({
    email: new FormControl<string>(''),
    name: new FormControl<string>(''),
});

I would like to use a DTO and do this:

export interface UserDto {
  name: string;
  email: string;
}

const userForm = new FormGroup<FormGroupOf<UserDto>>({
    email: new FormControl<string>(''),
    name: new FormControl<string>(''),
});

The trick here is to use a little generic utility type called FormGroupOf<T> and recursively map properties of the DTO to either controls, groups, or an array of groups.

form-group-of.ts
export type FormGroupOf<T> = {
  [key in keyof T]: T[key] extends Array<infer TArray>
    ? FormArray<
        TArray extends object
          ? FormGroup<FormGroupOf<TArray>>
          : FormControl<TArray | null | undefined>
      >
    : T[key] extends object
    ? FormGroup<FormGroupOf<T[key]>>
    : FormControl<T[key] | null | undefined>;
};

This would be especially useful when creating a dummy form component that gets a value of the form as @Input and @Outputs the updated form on the save button click.

User-form.component.html
<form [formGroup]="form">
  <span>
    Name:
    <input type="text" [formControl]="form.controls.name" />
  </span>
  <span>
    Email:
    <input type="email" [formControl]="form.controls.email" />
  </span>

  <div style="margin-top: 2rem;">
    <button (click)="save()" [disabled]="form.pristine">Save Changes</button>
  </div>
</form>
User-form.component.ts
@Component({
  selector: 'user-form',
  templateUrl: './user-form.component.html',
})
export class UserFormComponent {
  @Input()
  set value(v: UserDto | null) {
    this.form.reset(v);
  }

  @Output()
  readonly saved = new EventEmitter<UserDto>();

  readonly form = new FormGroup<FormGroupOf<UserDto>>({
    name: new FormControl<string>(''),
    email: new FormControl<string>(''),
  });

  save() {
    if (this.form.valid) {
      this.saved.emit(this.form.getRawValue());
    }
  }
}

The form component can be then displayed in a view using a fetched DTO from the backend and act upon an emitted event on the save button click. You can imagine the usage of such a component in a view as following:

app.component.html
<user-form [value]="user$ | async" (saved)="userSaved($event)"></user-form>
App.component.ts
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  readonly user$ = of(<UserDto>{
    name: 'John Doe',
    contact: { email: 'john@doe.com' },
    friends: [{ name: 'Jane Doe' }],
  }).pipe(delay(300)); // Mocks backend call

  userSaved(user: UserDto) {

    // TODO: Handle save
    alert('Saving user:\n\n' + JSON.stringify(user));
  }
}

What do you think? Are you doing something similar already or do you think this is not the best approach? Let me know in the comments and if you are interested in a more advanced sample that is using nested objects and an array of objects, have a look at this Stackblitz sample.

Hope it helps!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published.