Angular
·4 min read·↗

Using Strictly Typed Reactive Forms in Angular

Main cover illustration for article: Using Strictly Typed Reactive Forms in Angular
Summarize with AI:

⚡ Using Modern Angular (v17+)? Jump directly to 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:

// ⚠️ 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:

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():

// 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:

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

FeatureLegacy Untyped Forms (v2 – v13)Modern Typed Reactive Forms (v14+)
Control Accessform.get('name')?.value (any)form.controls.name.value (string)
Typo Protection❌ Fails silently at runtime✅ Caught at compile-time
Form ResetResets to nullResets to default initial value via nonNullable
Raw Value InferenceanyExact TypeScript interface via form.getRawValue()
Dynamic FieldsUntyped FormGroupType-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.
Part of the Angular Series

These are my experiences learning and facing my daily challenges working with Angular.

View Entire Series

Frequently Asked Questions

What is the main benefit of Typed Reactive Forms in Angular?

Typed Reactive Forms provide compile-time type safety across FormControl, FormGroup, and FormArray. TypeScript catches mismatched types or misspelled control names during build rather than failing silently at runtime.

What does the nonNullable option or NonNullableFormBuilder do in Angular?

By default, calling form.reset() resets control values to null. Using nonNullable: true or inject(NonNullableFormBuilder) ensures controls reset to their initial initial values and prevents null from being added to their TypeScript type union.

What is the difference between FormGroup and FormRecord in Angular?

FormGroup is used for forms with fixed, predefined keys. FormRecord is used for dynamic forms where keys are unknown or added dynamically at runtime while all controls share the same type.

How do you access typed controls directly without get() in Angular?

With Typed Forms, you can access controls directly via property dot-notation: form.controls.email.value, providing full autocompletion and strict type inference without risking typos in string keys.

Related Articles

Share this article

If you found this guide helpful, consider sharing it with your team or fellow developers.


Real Software. Real Lessons.

I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.

User avatar
User avatar
User avatar
User avatar
+13K

Join 13,800+ developers and readers.

No spam ever. Unsubscribe at any time.