In Angular, what do the booleanAttribute and numberAttribute input transforms do, and why does <app-avatar size="64" disabled> need them?
answer
- static attributes arrive as strings
- presence means true, except 'false'
- NaN fallback for bad numbers
- transform parameter types the template
basics
~20 sThey 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 sA 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 linesimport { 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
Know that booleanAttribute and numberAttribute exist and that static attributes pass strings to inputs.
Explain the booleanAttribute rules including 'false', the NaN fallback, and that the transform's parameter type decides what templates may bind.
Design a component library's input API so static attributes work, keeping transforms pure and moving derivations into computed().
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.