With Angular's strictTemplates enabled, what type does $event get in (keydown), (keydown.enter) and a component output binding, and why?
answer
- the compiler writes a type-check block
- strictDomEventTypes and an event map
- unknown names fall back
- void elements get a typed target
basics
~10 sUnder 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 sWith `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 linesimport { 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
Recall that strict templates give $event a real type, and that outputs use their declared emitted type.
Explain the addEventListener rewrite in the type-check block, the fallback to Event for key filters, and void-element target narrowing.
Show how you keep strict templates on in a legacy codebase: template references over $any, Event-typed handlers for key filters.
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