---
title: "Using Strictly Typed Reactive Forms in Angular"
date: "2022-12-01"
slug: "using-strictly-typed-reactive-forms-in-angular"
author: "Dany Paredes"
canonical: "https://danywalls.com/using-strictly-typed-reactive-forms-in-angular"
description: "Learn how to use strictly typed reactive forms in Angular, leverage NonNullableFormBuilder and FormRecord, and eliminate runtime nullability bugs with compile-time safety."
---


> ⚡ **Using Modern Angular (v17+)?** Jump directly to [Modern Typed Forms with NonNullableFormBuilder & inject()](#modern-typed-forms-with-nonnullableformbuilder--inject) to see idiomatic type-safe form setup.

Before Angular 14, Reactive Forms were completely untyped (`any`). Accessing a form control with `form.get('emial')` wouldn't throw a compile-time error if you had a typo—it would simply fail silently or throw a runtime `TypeError` when reading `.value`.

With **Strictly Typed Reactive Forms**, Angular brings full compile-time type safety to `FormControl`, `FormGroup`, `FormArray`, and `FormRecord`.

In this practical guide, I will show you how to migrate from legacy untyped forms to strictly typed reactive forms, how to use `NonNullableFormBuilder`, and how to prevent runtime nullability issues.

---

## The Problem: The Untyped Reactive Forms Trap

Let's look at what went wrong in legacy untyped Angular forms:

```typescript
// ⚠️ Untyped Angular Forms (v2 – v13)
export class CheckoutComponent {
  purchaseForm = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
    amount: new FormControl(0),
  });

  submit() {
    // 1. String-based get() has zero typo protection:
    const amountVal = this.purchaseForm.get('amout')?.value; // Typo in string! Compiles fine, fails at runtime.

    // 2. Value was typed as 'any', forcing unsafe operations:
    const total = amountVal + 5; // Might accidentally concatenate string if API returned string!
  }
}
```

Even worse, when calling `this.purchaseForm.reset()`, Angular would reset every control value to `null`, turning `{ name: string }` into `{ name: string | null }`.

---

## 1. Defining Strict Types with `FormControl<T>`

In modern Angular, `FormControl` accepts a generic type representing the value it holds:

```typescript
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

export interface PurchaseForm {
  name: FormControl<string>;
  email: FormControl<string>;
  amount: FormControl<number>;
  newsletter: FormControl<boolean | null>; // Explicitly optional/nullable
}

@Component({
  selector: 'app-checkout',
  templateUrl: './checkout.component.html',
})
export class CheckoutComponent {
  purchaseForm = new FormGroup<PurchaseForm>({
    name: new FormControl('', { nonNullable: true }),
    email: new FormControl('', { nonNullable: true }),
    amount: new FormControl(0, { nonNullable: true }),
    newsletter: new FormControl(false),
  });

  submit(): void {
    // Direct dot-notation with full autocomplete & strict types:
    const name: string = this.purchaseForm.controls.name.value;
    const amount: number = this.purchaseForm.controls.amount.value;

    console.log(`Submitting order for ${name} totaling $${amount}`);
  }
}
```

---

## Modern Typed Forms with NonNullableFormBuilder & inject()

Creating `new FormControl(val, { nonNullable: true })` for every single field can feel verbose.

In modern Angular (v17+ / v19), we use **`NonNullableFormBuilder`** combined with **`inject()`**:

```typescript
// checkout.component.ts (Modern Standalone & Typed Forms)
import { Component, inject } from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  selector: 'app-checkout',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label for="name">Full Name</label>
        <input id="name" type="text" formControlName="name" />
      </div>

      <div class="form-group">
        <label for="email">Email Address</label>
        <input id="email" type="email" formControlName="email" />
      </div>

      <div class="form-group">
        <label for="amount">Amount ($)</label>
        <input id="amount" type="number" formControlName="amount" />
      </div>

      <button type="submit" [disabled]="form.invalid">Submit Payment</button>
    </form>
  `,
})
export class CheckoutComponent {
  // NonNullableFormBuilder automatically creates nonNullable controls for every field!
  private fb = inject(NonNullableFormBuilder);

  readonly form = this.fb.group({
    name: ['', [Validators.required]],
    email: ['', [Validators.required, Validators.email]],
    amount: [10, [Validators.required, Validators.min(1)]],
  });

  onSubmit(): void {
    if (this.form.invalid) return;

    // form.getRawValue() produces a clean, non-nullable strongly-typed object:
    // { name: string; email: string; amount: number }
    const payload = this.form.getRawValue();
    console.log('Valid Payload:', payload);
  }
}
```

---

## Dynamic Key Safety: `FormRecord`

If your form has dynamic keys (such as user-defined custom settings or role checkboxes where keys are generated dynamically), use **`FormRecord`** instead of `FormGroup`:

```typescript
import { Component, inject } from '@angular/core';
import { FormRecord, FormControl, NonNullableFormBuilder } from '@angular/forms';

@Component({
  selector: 'app-permissions',
  standalone: true,
  template: `...`,
})
export class PermissionsComponent {
  private fb = inject(NonNullableFormBuilder);

  // FormRecord allows adding and removing controls dynamically with strict value types
  permissionsRecord: FormRecord<FormControl<boolean>> = this.fb.record({
    canRead: this.fb.control(true),
    canWrite: this.fb.control(false),
  });

  addPermission(permissionKey: string): void {
    // Type safe: only accepts FormControl<boolean>
    this.permissionsRecord.addControl(permissionKey, this.fb.control(false));
  }
}
```

---

## Typed Forms vs Untyped Forms Comparison

| Feature | Legacy Untyped Forms (v2 – v13) | Modern Typed Reactive Forms (v14+) |
| :--- | :--- | :--- |
| **Control Access** | `form.get('name')?.value` (`any`) | `form.controls.name.value` (`string`) |
| **Typo Protection** | ❌ Fails silently at runtime | ✅ Caught at compile-time |
| **Form Reset** | Resets to `null` | Resets to default initial value via `nonNullable` |
| **Raw Value Inference** | `any` | Exact TypeScript interface via `form.getRawValue()` |
| **Dynamic Fields** | Untyped `FormGroup` | Type-safe `FormRecord<FormControl<T>>` |

---

## Summary

Typed Reactive Forms eliminate an entire class of runtime bugs in Angular applications:

1. Use **`NonNullableFormBuilder`** via `inject(NonNullableFormBuilder)` for clean, non-nullable forms.
2. Access controls directly through **`form.controls.<fieldName>`** to get autocomplete and instant type checking.
3. Use **`form.getRawValue()`** to extract clean TypeScript objects for your HTTP payloads.
4. Use **`FormRecord`** when dealing with dynamic dictionary-like form controls.

