skip to content

In Angular's typed reactive forms, why is new FormControl('') inferred as FormControl<string | null>, and how do you remove the null?

level: juniorimportance: must knowfreq 62%

answer

  1. what reset() does by default
  2. the default value is null
  3. an option on the control
  4. nonNullable: true, or fb.nonNullable

basics

~20 s

Calling reset() on a plain FormControl sets its value to null, so Angular adds null to the inferred type. Passing {nonNullable: true} makes reset() return to the initial value instead, and the type becomes FormControl<string>.

solid answer

~40 s

Since Angular 14, reactive forms are strictly typed, and `new FormControl('')` infers `FormControl<string | null>`. The `null` is not noise: by default a control's `defaultValue` is `null`, so `reset()` with no argument sets the value to `null`, and the type has to admit that. You remove it with `new FormControl('', {nonNullable: true})`, which makes the initial value the default, so `reset()` restores `''` and the type narrows to `FormControl<string>`. For a whole form, `fb.nonNullable.group({...})` or an injected `NonNullableFormBuilder` applies the option to every control it creates from a raw value. The one case where you need an explicit generic is a control that starts at `null`: `new FormControl(null)` infers `FormControl<null>`, so you write `new FormControl<string | null>(null)`.

code

ts · 12 lines
ts
import {FormControl} from '@angular/forms';

const plain = new FormControl('Ada');
plain.reset();
console.log(plain.value); // null  (type: string | null)

const strict = new FormControl('Ada', {nonNullable: true});
strict.reset();
console.log(strict.value); // 'Ada' (type: string)

strict.reset('Grace');
console.log(strict.value); // 'Grace' - an explicit value wins

go deeper

for a junior

Recall that a plain FormControl resets to null, which is why null is in its type, and that nonNullable: true makes it reset to the initial value instead.

for a middle

Explain the defaultValue mechanism behind reset(), the NonNullableFormBuilder shortcut, and why a control that starts at null needs an explicit generic.

for a senior

Show that nonNullable is a behavioural change, not a type annotation: audit reset() callers before flipping it, and remember value accessors such as the number input can still write null.

for a principal

Frame the choice as a team convention: non-nullable builders by default for new forms, explicit nullable types only where empty is a real domain state.

## What strict typing infers Since **Angular 14**, the reactive forms classes in `@angular/forms` carry a generic type parameter for their value. `FormControl<TValue>` knows the type of `value`, `valueChanges`, `setValue()` and `reset()`. You usually do not write the generic yourself: TypeScript infers it from the first constructor argument. ```ts import {FormControl} from '@angular/forms'; const displayName = new FormControl(''); // FormControl<string | null> const theme = new FormControl('dark', {nonNullable: true}); // FormControl<string> const nickname = new FormControl<string | null>(null); // explicit: starts empty ``` The surprise for most people is the first line. The initial value is a string, yet the type includes `null`. ## Why `null` is in the type The reason is a **runtime** behaviour, not a typing quirk. Every `FormControl` has a `defaultValue`. Unless you opt out, that default is `null`, and `reset()` called without an argument puts the control back to its default: ```ts const displayName = new FormControl('Ada'); displayName.reset(); console.log(displayName.value); // null ``` Because any code path can call `reset()`, including a parent `FormGroup` being reset, the control's value really can become `null` at any moment. The type system reports that honestly. With `strictNullChecks` on, TypeScript then forces you to handle `null` wherever you read the value. ## Removing it: `nonNullable` The `nonNullable` option changes both the type **and** the runtime behaviour: - The constructor overload that takes `{nonNullable: true}` returns `FormControl<T>` without `null`. - The control's `defaultValue` becomes the initial value, so `reset()` restores it instead of writing `null`. - A value passed to `reset(value)` still wins over the default. - `reset()` also accepts an `overwriteDefaultValue` option that makes the value you reset to the new default. The older spelling `initialValueIsDefault` does the same thing and is **deprecated** in favour of `nonNullable`. | Declaration | Inferred type | `reset()` gives | |---|---|---| | `new FormControl('')` | `FormControl<string \| null>` | `null` | | `new FormControl('', {nonNullable: true})` | `FormControl<string>` | `''` | | `new FormControl(null)` | `FormControl<null>` | `null` | | `new FormControl<string \| null>(null)` | `FormControl<string \| null>` | `null` | ## Doing it for a whole form Writing `{nonNullable: true}` on every control is noisy. `FormBuilder` exposes a `nonNullable` property that returns a `NonNullableFormBuilder`, and you can also inject `NonNullableFormBuilder` directly. Every control it creates implicitly from a raw value is non-nullable: ```ts import {Component, inject} from '@angular/core'; import {NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; @Component({selector: 'app-settings', imports: [ReactiveFormsModule], template: ''}) export class Settings { private readonly fb = inject(NonNullableFormBuilder); readonly form = this.fb.group({displayName: '', theme: 'dark'}); // form.controls.displayName: FormControl<string> } ``` The plain `FormBuilder`, by contrast, builds `FormControl<T | null>` for each raw value, exactly like the constructor. ## Resets that arrive through a parent A control is rarely reset on its own. Calling `reset()` on a `FormGroup` with no argument resets **each child** to that child's own default. In one group you can therefore get mixed results: - children created with `nonNullable` return to their initial values; - plain children become `null`; - values you pass to the group's `reset({...})` win for the keys you name. This is why the nullability decision belongs to each control rather than to the form as a whole, and why a builder that applies `nonNullable` uniformly makes a form's reset behaviour much easier to predict. It also explains why the `null` appears in the type of a control you never reset directly: its parent can do it for you. ## The explicit-generic case Inference works from the initial value, so a control that **starts empty** needs help. `new FormControl(null)` infers `FormControl<null>`, a type whose only legal value is `null`, and a later `setValue('x')` fails to compile. Write the generic: `new FormControl<string | null>(null)`. ## Common pitfalls 1. Silencing the error with `!` or `as string` instead of choosing `nonNullable`. The value can still be `null` after a reset, so the assertion lies. 2. Treating `nonNullable` as a runtime guarantee. It only controls the default used by `reset()`. A value accessor can still write `null`: an emptied `<input type="number">` reports `null` to its control. 3. Flipping `nonNullable` on an existing form without checking callers of `reset()`. Code that expected an empty control after reset now gets the initial value back. 4. Expecting template-driven forms to be typed. Strict typing covers reactive forms only; `ngModel` values are not inferred this way. The short version: `null` is in the type because `reset()` can put it there, and `nonNullable` removes it by changing what `reset()` does.

  • Does nonNullable guarantee the control never holds null at runtime?
    No. It sets the control's default to its initial value, so `reset()` never writes `null`, and it narrows the type. A value accessor can still push `null`: Angular's number accessor reports an emptied `<input type="number">` as `null`. Treat `nonNullable` as a reset rule plus a type, and validate inputs that can be cleared.
  • Why does new FormControl(null) break a later setValue('text') call?
    Inference works from the initial value, and `null` alone infers `FormControl<null>`, whose value type admits nothing but `null`. Declare the wider type explicitly with `new FormControl<string | null>(null)`, and `setValue('text')` compiles.
  • How do you make every control in a builder-created group non-nullable without repeating the option?
    Use the non-nullable builder: `fb.nonNullable.group({...})` on a `FormBuilder`, or inject `NonNullableFormBuilder` and call `group()`. Each control it creates implicitly from a raw value gets `nonNullable` set, so its type drops `null` and `reset()` restores the initial value.

saying these in an interview costs you the question

  • The null in the type is a TypeScript inference bug to cast away.
  • nonNullable only changes the type and has no runtime effect.
  • reset() always restores a control's initial value.
  • new FormControl(null) infers FormControl<any>, so setValue accepts anything.
  • nonNullable makes it impossible for the control to ever hold null.