skip to content

In Angular, what do the booleanAttribute and numberAttribute input transforms do, and why does <app-avatar size="64" disabled> need them?

level: middleimportance: should knowfreq 44%

answer

  1. static attributes arrive as strings
  2. presence means true, except 'false'
  3. NaN fallback for bad numbers
  4. transform parameter types the template

basics

~20 s

They coerce bound values before the input stores them: booleanAttribute makes a present attribute true unless it is 'false', and numberAttribute parses strings, falling back to NaN. Static attributes pass strings, so size and disabled need them.

solid answer

~40 s

A static attribute passes a string: `size="64"` sends `'64'` and a bare `disabled` sends `''`. With `input(40)` and `input(false)` those are type errors under `strictTemplates`, and without strict checking `''` is falsy, so `disabled` would not disable. Declaring `input(false, { transform: booleanAttribute })` makes presence mean `true`, with the string `'false'` and `null` mapping to `false`. `input(40, { transform: numberAttribute })` converts `'64'` to `64` and anything unparseable to `NaN`, or to a fallback you pass as its second argument. The transform's parameter type decides what templates may bind, so both built-ins accept `unknown`. Transforms must be static, pure functions, and they run only on bound values, not on the default.

code

ts · 15 lines
ts
import { Component, booleanAttribute, input, numberAttribute } from '@angular/core';

@Component({
  selector: 'app-avatar',
  template: `
    <img [src]="src()" [alt]="name()" [width]="size()" [height]="size()"
         [class.dimmed]="disabled()" />
  `,
})
export class Avatar {
  readonly src = input.required<string>();
  readonly name = input('');
  readonly size = input(40, { transform: numberAttribute });
  readonly disabled = input(false, { transform: booleanAttribute });
}

go deeper

for a junior

Know that booleanAttribute and numberAttribute exist and that static attributes pass strings to inputs.

for a middle

Explain the booleanAttribute rules including 'false', the NaN fallback, and that the transform's parameter type decides what templates may bind.

for a senior

Design a component library's input API so static attributes work, keeping transforms pure and moving derivations into computed().

for a principal

Set conventions for a shared UI kit: which inputs accept attribute-style usage, how fallbacks are chosen, and how strict template checking is enforced.

## The problem transforms solve In Angular, an input can be set in two ways from a parent's template: - a **property binding**, `[size]="64"`, which passes the value of an expression, here the number `64`; - a **static attribute**, `size="64"` or just `disabled`, which passes the attribute's **string** value: `'64'`, or `''` for a bare attribute. Static attributes are the natural way to use a presentational component such as an avatar: `<app-avatar src="/u/7.png" size="64" disabled />`. But with `size = input(40)` typed `number` and `disabled = input(false)` typed `boolean`, those strings are the wrong type. With `strictTemplates` (on by default for new projects), the compiler's `strictAttributeTypes` check rejects `size="64"` against a `number` input. Without strict checking the string slips through and `disabled` receives `''`, which is **falsy**, so the bare attribute would *disable nothing*. An **input transform** is a function Angular runs on every value bound to the input before storing it. It converts whatever the parent passed into the type the component wants. ## The two built-in transforms `@angular/core` ships two, matching the common cases: | Bound value | `booleanAttribute` result | `numberAttribute` result | |---|---|---| | `''` (bare attribute) | `true` | `NaN` | | `'true'` / `'disabled'` | `true` | `NaN` | | `'false'` | `false` | `NaN` | | `null` / `undefined` | `false` | `NaN` | | `'64'` | `true` | `64` | | `'64px'` | `true` | `NaN` | | `true` / `64` | `true` / `true` | `NaN` / `64` | The rules behind the table: - **`booleanAttribute`** mirrors HTML boolean attributes, where *presence* means true, with one Angular twist: the literal string `'false'` becomes `false`. Anything that is already a boolean passes through; `null` and `undefined` become `false`. - **`numberAttribute`** returns `Number(value)` when the value is a clean number and a **fallback** otherwise. The fallback defaults to `NaN`; the function takes a second `fallbackValue` argument, so a wrapper such as `(v: unknown) => numberAttribute(v, 40)` supplies a sensible default. ## Declaring and typing a transform ```ts import { Component, booleanAttribute, input, numberAttribute } from '@angular/core'; @Component({ selector: 'app-avatar', template: `<img [src]="src()" [width]="size()" [class.dimmed]="disabled()" />`, }) export class Avatar { readonly src = input.required<string>(); readonly size = input(40, { transform: numberAttribute }); readonly disabled = input(false, { transform: booleanAttribute }); } ``` With a transform, the input has **two types**: the type the component *reads* (`number`, `boolean`) and the type the template may *write*, which is the transform's **parameter type**. Both built-ins take `unknown`, so any binding type-checks. The signal's type becomes `InputSignalWithTransform<number, unknown>`. A custom transform declared as `function appendPx(value: number): string` accepts only numbers in templates and gives the component a string. The decorator form is the same option: `@Input({ transform: booleanAttribute }) disabled = false;`. ## Rules for transform functions 1. **Statically analysable.** The compiler must see the function reference at build time; it cannot be chosen conditionally or computed. 2. **Pure.** The Angular docs say transforms should always be pure functions; relying on outside state makes the input unpredictable. 3. **Only on bound values.** The transform runs when a value is bound, not on the default you pass to `input()`, so the default must already be of the read type. 4. **Not for model inputs.** `model()` does not support transforms. ## When not to use one A transform is a coercion, not validation or business logic. Keep it to shape conversions. If the component needs to derive something from the value, such as a CSS class from a size, derive it with `computed()` from the transformed input rather than folding it into the transform. Prefer transforms over the older **setter-based** `@Input` pattern: the docs recommend transforms because setters can run repeatedly and invite side effects.

  • How would you make an invalid size fall back to 40 instead of NaN?
    `numberAttribute` takes a second `fallbackValue` argument, so pass a wrapper: `size = input(40, { transform: (v: unknown) => numberAttribute(v, 40) })`. The wrapper is still a static, pure function, which is what the compiler requires of a transform.
  • Does the transform run on the default value passed to input()?
    No. Angular runs the transform when a value is bound to the input. The default is stored as given, so it must already be of the type the component reads, such as `false` for a boolean input.

saying these in an interview costs you the question

  • booleanAttribute turns the string 'false' into true because it is non-empty.
  • A bare disabled attribute sets a boolean input to true without any transform.
  • numberAttribute returns 0 when the value cannot be parsed.
  • A transform can be chosen at runtime based on configuration.
  • Transforms are a good place for validation and side effects.