skip to content

With Angular's strictTemplates enabled, what type does $event get in (keydown), (keydown.enter) and a component output binding, and why?

level: middleimportance: should knowfreq 40%

answer

  1. the compiler writes a type-check block
  2. strictDomEventTypes and an event map
  3. unknown names fall back
  4. void elements get a typed target

basics

~10 s

Under strictTemplates the compiler infers $event from TypeScript's DOM event maps, so (keydown) gives a KeyboardEvent, while a keyed pseudo-event like keydown.enter falls back to plain Event. Outputs use their declared emitted type.

solid answer

~40 s

With `strictTemplates`, Angular's type checker turns each native event binding into a call like `element.addEventListener('input', $event => ...)` in a generated type-check block, so TypeScript infers `$event` from `HTMLElementEventMap`. A known name gets its mapped type: `(keydown)` a `KeyboardEvent`, `(focus)` a `FocusEvent`. A name the map does not contain, including key pseudo-events such as `keydown.enter` and custom events, falls back to `Event`, so passing `$event` to a method typed `KeyboardEvent` is an error. `$event.target` is normally `EventTarget | null`; on current versions the compiler narrows it to the element type only for void elements such as `<input>`. For a component or directive output, `$event` is the output's emitted type. With the check off, `$event` is `any`.

code

ts · 25 lines
ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-search-box',
  template: `
    <!-- void element: $event.target is narrowed to HTMLInputElement -->
    <input (input)="query.set($event.target.value)" (keydown.enter)="submit($event)" />

    <!-- not void: use a template reference instead -->
    <textarea #notes (input)="setNotes(notes.value)"></textarea>
  `,
})
export class SearchBox {
  query = signal('');
  notes = signal('');

  // keydown.enter is not in the event map, so $event is typed Event
  submit(event: Event) {
    event.preventDefault();
  }

  setNotes(value: string) {
    this.notes.set(value);
  }
}

go deeper

for a junior

Recall that strict templates give $event a real type, and that outputs use their declared emitted type.

for a middle

Explain the addEventListener rewrite in the type-check block, the fallback to Event for key filters, and void-element target narrowing.

for a senior

Show how you keep strict templates on in a legacy codebase: template references over $any, Event-typed handlers for key filters.

for a principal

Weigh turning strictTemplates on in a large app: the error backlog it exposes against the runtime bugs it prevents.

## How Angular type-checks a listener Angular's compiler checks templates by generating an internal TypeScript function, a **type-check block**, that mirrors the template, and letting TypeScript report errors on it. How an event binding is written into that block decides the type of `$event`. The flags involved: - **`strictTemplates`** turns on the full set of template checks, including the two below. - **`strictDomEventTypes`** controls native DOM events. Its documented default is `false` on its own, but `strictTemplates` switches it on. New CLI projects enable `strictTemplates`. - **`strictOutputEventTypes`** controls outputs of components and directives, and is also switched on by `strictTemplates`. ## Native DOM events With DOM event checking on, the compiler emits a real `addEventListener` call on a typed element variable: ```ts // what the type-check block does for <button (click)="select($event)"> const el = document.createElement('button'); el.addEventListener('click', ($event) => { this.select($event); }); ``` TypeScript's own overloads then infer `$event` from `HTMLElementEventMap`. For `window:` and `document:` targets the call is made on `window` or `document`, using their event maps. | Binding | Name looked up | `$event` type | |---|---|---| | `(keydown)` | `keydown` | `KeyboardEvent` | | `(focus)` | `focus` | `FocusEvent` | | `(click)` | `click` | the mouse or pointer event type your TypeScript DOM lib maps it to | | `(keydown.enter)` | `keydown.enter` | `Event` (not in the map) | | `(document:keydown.control.k)` | `keydown.control.k` | `Event` | | `(my-custom-thing)` | `my-custom-thing` | `Event` | The row that surprises people is the key filter. The name is passed through whole, and `keydown.enter` is not a key of the event map, so TypeScript's fallback overload applies and `$event` is `Event`. A method declared `onEnter(e: KeyboardEvent)` then fails with *Argument of type 'Event' is not assignable to parameter of type 'KeyboardEvent'*. Fixes: type the parameter `Event` (enough for `preventDefault()`), narrow inside the method with `instanceof KeyboardEvent`, or bind to plain `(keydown)` and check the key yourself. ## The `target` problem, and void elements `Event.target` is typed `EventTarget | null`, because an event can bubble from any descendant or be dispatched manually. That is why the classic `(input)="setName($event.target.value)"` fails under strict checking with *Object is possibly 'null'* or *Property 'value' does not exist on type 'EventTarget'*. The compiler now narrows this in one safe case. For a **void element** (an element that cannot have children, such as `<input>` or `<img>`) no event can have bubbled up from inside it, so the type-check block adds an assertion that `$event.target` is that element. On `<input>`, `$event.target.value` then type-checks as an `HTMLInputElement` property. The compiler enables this when the project's `@angular/core` is 20.2 or later. It does **not** apply to `<textarea>`, `<select>` or a `<div>` wrapper, because those can contain children. Where narrowing does not apply, the usual options are: 1. Use a template reference: `<textarea #box (input)="setName(box.value)">`. 2. Accept `Event` in the method and cast inside TypeScript, where a cast is visible and reviewable. 3. Use `$any($event.target).value`, which switches checking off for that expression and is best kept rare. ## Outputs For a binding that matches an output, the type-check block subscribes to the output with a typed callback, so `$event` is the output's type parameter. `selected = output<Product>()` gives `$event: Product`; `closed = output<void>()` gives `void`. An `EventEmitter<T>` output behaves the same. Mistyping the handler parameter is then a compile error rather than a runtime surprise. ## Choosing a fix | Situation | Preferred fix | Why | |---|---|---| | Reading a value from `<input>` | `$event.target.value` directly | narrowed to the element on current versions | | Reading a value from `<textarea>` or `<select>` | a `#ref` and `ref.value` | the reference is typed as the element | | Key filter handler needs `preventDefault()` | parameter typed `Event` | `Event` already has the method | | Key filter handler needs `key` or `repeat` | `instanceof KeyboardEvent` inside the method | a checked narrowing, not a blind cast | The type-check block is never executed; it exists only so TypeScript can report errors against the template. So these choices change what the compiler accepts, not what happens at runtime, where `$event` is always the real event the browser dispatched. ## When checking is off With `strictTemplates` off (and the individual flags not set), `$event` is `any` for DOM events. Everything compiles, and mistakes like reading `.value` on a `PointerEvent` only appear at runtime. That is the main argument for keeping strict templates on when migrating older projects.

  • Why does (input)="set($event.target.value)" type-check on an <input> but fail on a <textarea> in current Angular?
    The compiler asserts `$event.target` is the bound element only for void elements, which cannot have children, so no event could have bubbled from inside them. `<input>` is void and gets `HTMLInputElement`; `<textarea>` is not, so `target` stays `EventTarget | null`. Use a template reference like `#box` and pass `box.value` instead.
  • What type is $event in an Angular binding to an output declared as closed = output<void>()?
    `void`, taken from the output's type parameter. Handlers normally ignore it, and a method expecting an argument there is a type error under strict output checking.

saying these in an interview costs you the question

  • (keydown.enter) gives $event the type KeyboardEvent
  • $event.target is always typed as the element it is bound on
  • $event is typed any even with strictTemplates enabled
  • An output's $event is an Event wrapping the emitted value
  • $any() is the recommended everyday fix for target typing