TypeScript 90 🔷 TypeScript with Angular
Angular was rewritten in TypeScript before its 2.0 release, and every version since has deepened the integration. The result is a framework where the template compiler understands the types declared in your components, and the editor autocompletes bindings, inputs, and event arguments from the same source of truth. Angular’s template type checker is not a separate tool — it is the TypeScript compiler extended to understand Angular’s template syntax .
The Angular documentation states it plainly: “TypeScript is the primary language for Angular application development” . The strictTemplates flag in tsconfig.json controls how aggressively the compiler checks template bindings against the types declared in the component class. With strict mode enabled, [user]="selectedUser" is checked against the user input’s type, and $event in an event binding has the correct type for the event it receives .
Key point: Angular’s TypeScript integration happens in two places. The component class declares inputs, outputs, and state with TypeScript types. The template is type-checked against those declarations. The strictTemplates flag enables the full check. Without it, templates are checked in a weaker mode where any is permitted in many positions .
Why Angular and TypeScript work well together
Angular’s architecture was designed around TypeScript from the beginning. Dependency injection, decorators, and template bindings all rely on type information that only TypeScript provides.
The template type-checking problem. A template binding like {{ user.name }} assumes user has a name property. Without type checking, a typo like {{ user.nmae }} compiles and fails at runtime. Angular’s template type checker treats the template as TypeScript code, resolving user to its declared type and reporting errors at compile time . The strictTemplates flag enables this check .
The input contract problem. A component declares @Input() user: User. The parent’s template passes [user]="selectedUser". The template type checker verifies that selectedUser is assignable to User. If the parent passes a string or a null where User is required, the compiler reports the error before the application runs .
The event type problem. A DOM event binding like (click)="handleClick($event)" passes a MouseEvent to the handler. Without strict mode, $event is any. With strictDomEventTypes enabled, $event is typed as MouseEvent, and the handler signature must accept it .
The form typing problem. Angular 14 introduced strictly typed reactive forms. FormControl<string> enforces that value is always a string (or null unless nonNullable: true). FormGroup infers its value type from its controls. This eliminates the any that previously existed in form APIs .
The trade-off. Strict template checking catches more errors but also surfaces false positives from libraries not written with strictNullChecks in mind. Angular provides granular flags to disable specific checks without disabling all of them .
a. Template Type Checking and strictTemplates
The strictTemplates flag in tsconfig.json under angularCompilerOptions enables full template type checking. It is a superset of fullTemplateTypeCheck, which was the older, less strict mode .
{
"angularCompilerOptions": {
"strictTemplates": true
}
}
With strictTemplates: true, the compiler checks input bindings, DOM event types, safe navigation types, local reference types, and generic context. The individual strictness flags inherit from strictTemplates unless explicitly overridden .
The most important flag to understand is strictNullInputTypes, which controls whether strictNullChecks is honored when checking @Input() bindings. If a library was not built with strictNullChecks, its declaration files may not indicate nullable fields. Turning strictNullInputTypes off allows binding nullable values to those inputs without errors .
// With strictTemplates, this is an error if User | null is bound to User
<user-detail [user]="selectedUser"></user-detail>
// The fix is to handle the null case in the parent
selectedUser: User | null = null;
// Or use the non-null assertion in the template
<user-detail [user]="user!"></user-detail>
The $any() function opts out of type checking for a specific expression. It is the escape hatch when a library’s types are wrong or incomplete .
b. Signal Inputs, Queries, and Model Inputs
Angular 17.1 introduced signal-based inputs (input()), and 17.2 added signal queries (viewChild, viewChildren) and model inputs (model()) . These APIs replace the decorator-based versions with functions that return signals.
Signal inputs are declared with input() and input.required():
import { Component, input } from '@angular/core';
@Component({
selector: 'user-detail',
template: '{{ user().name }}',
})
export class UserDetailComponent {
user = input.required<User>();
}
The user is a Signal<User>. In the template, user() returns the value. The template type checker verifies the binding against User . The .required() variant throws if the input is not provided.
Signal queries replace @ViewChild, @ViewChildren, @ContentChild, and @ContentChildren :
import { Component, viewChild, ElementRef } from '@angular/core';
@Component({
template: `<input #searchInput />`,
})
export class SearchComponent {
searchInput = viewChild<ElementRef>('searchInput');
focus() {
this.searchInput()?.nativeElement.focus();
}
}
The query returns a Signal<ElementRef | undefined>. The .required() variant returns Signal<ElementRef> and throws if not found . Queries are undefined until AfterViewInit or AfterContentInit .
Model inputs enable two-way binding with signals :
import { Component, model } from '@angular/core';
@Component({
selector: 'custom-checkbox',
template: `<input type="checkbox" [checked]="checked()" (change)="toggle()">`,
})
export class CustomCheckbox {
checked = model(false);
toggle() {
this.checked.set(!this.checked());
}
}
The parent uses [(checked)]="isChecked" for two-way binding. The model() function returns a WritableSignal<T> that can be set from inside the component or bound from outside .
c. Typed Reactive Forms and HTTP
Angular 14 made reactive forms strictly typed by default. The type of a FormControl is inferred from its initial value, and null is included unless nonNullable: true is specified .
import { FormControl, FormGroup, Validators } from '@angular/forms';
const email = new FormControl('', { nonNullable: true, validators: [Validators.required] });
// email.value: string (never null)
const login = new FormGroup({
email: new FormControl('', { nonNullable: true }),
password: new FormControl('', { nonNullable: true }),
});
// login.value: { email: string; password: string }
FormGroup infers its value type from its controls. FormRecord is the typed equivalent of an untyped FormGroup for dynamic keys. FormArray is typed by its element type .
HttpClient accepts a generic type that asserts the shape of the response body:
import { HttpClient } from '@angular/common/http';
this.http.get<User[]>('/api/users').subscribe(users => {
// users: User[]
});
The Angular documentation is explicit: “The generic type of request methods is a type assertion about the data returned by the server. HttpClient does not verify that the actual return data matches this type” . The generic is a promise to the compiler, not a runtime check. For untrusted data, use unknown and validate with an assertion function.
The responseType option changes the return type:
this.http.get('/images/dog.jpg', { responseType: 'arraybuffer' }).subscribe(buffer => {
// buffer: ArrayBuffer
});
observe: 'response' returns the full HttpResponse<T> including status and headers .
Complete Example Session
This session builds a typed user detail component with signal inputs, a typed form, and a typed HTTP service.
// ============================================
// PART 1: THE TYPED INPUT
// ============================================
import { Component, input } from '@angular/core';
export interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'user-detail',
template: '<h2>{{ user().name }}</h2><p>{{ user().email }}</p>',
})
export class UserDetailComponent {
user = input.required<User>();
}
// ============================================
// PART 2: THE TEMPLATE TYPE CHECK
// ============================================
@Component({
selector: 'app-root',
template: '<user-detail [user]="selectedUser" />',
})
export class AppComponent {
selectedUser: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
// With strictTemplates, passing a string or null here is a compile error
}
// ============================================
// PART 3: THE TYPED FORM
// ============================================
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'login-form',
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<input formControlName="password" type="password" />
<button type="submit">Login</button>
</form>
`,
})
export class LoginFormComponent {
loginForm = new FormGroup({
email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
password: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
});
onSubmit() {
const { email, password } = this.loginForm.getRawValue();
// email: string, password: string — no null
console.log(email, password);
}
}
// ============================================
// PART 4: THE TYPED HTTP SERVICE
// ============================================
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
// ============================================
// PART 5: THE UNKNOWN ALTERNATIVE
// ============================================
import { HttpErrorResponse } from '@angular/common/http';
getUserSafe(id: number): Observable<unknown> {
return this.http.get<unknown>(`/api/users/${id}`);
}
// The consumer must validate the unknown before using it.
// Use an assertion function (TypeScript 84) for the validation.
// ============================================
// PART 6: THE SIGNAL QUERY
// ============================================
import { viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'search-box',
template: '<input #searchInput type="text" />',
})
export class SearchBoxComponent {
searchInput = viewChild<ElementRef<HTMLInputElement>>('searchInput');
constructor() {
afterNextRender(() => {
this.searchInput()?.nativeElement.focus();
});
}
}
// ============================================
// PART 7: THE MODEL INPUT
// ============================================
import { model } from '@angular/core';
@Component({
selector: 'custom-checkbox',
template: '<input type="checkbox" [checked]="checked()" (change)="toggle()" />',
})
export class CustomCheckboxComponent {
checked = model(false);
toggle() {
this.checked.set(!this.checked());
}
}
// Parent: <custom-checkbox [(checked)]="isChecked" />
// isChecked: boolean, updated automatically
// ============================================
// PART 8: THE STRICT TSCONFIG
// ============================================
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true
},
"angularCompilerOptions": {
"strictTemplates": true,
"strictInputTypes": true,
"strictDomEventTypes": true,
"strictNullInputTypes": true
}
}
// ============================================
// PART 9: THE TEMPLATE EVENT TYPE
// ============================================
@Component({
selector: 'event-demo',
template: '<input (input)="onInput($event)" />',
})
export class EventDemoComponent {
onInput(event: Event) {
const target = event.target as HTMLInputElement;
console.log(target.value);
}
}
// With strictDomEventTypes, $event is typed as Event for DOM events.
// The target must be narrowed or cast to HTMLInputElement.
// ============================================
// PART 10: THE GENERIC COMPONENT
// ============================================
@Component({
selector: 'generic-list',
template: `
<ul>
@for (item of items(); track $index) {
<li (click)="select.emit(item)">{{ item }}</li>
}
</ul>
`,
})
export class GenericListComponent<T> {
items = input.required<T[]>();
select = output<T>();
}
The ten parts cover the typed input, the template type check, the typed form, the typed HTTP service, the unknown alternative, the signal query, the model input, the strict tsconfig, the template event type, and the generic component.
Quick Reference
The Template Type-Checking Flags
| Flag | Effect |
|---|---|
strictTemplates | Enable all template type checks |
strictInputTypes | Check @Input() binding assignability |
strictNullInputTypes | Honor strictNullChecks for inputs |
strictDomEventTypes | Type $event for DOM events |
strictOutputEventTypes | Type $event for @Output() |
strictSafeNavigationTypes | Type user?.name correctly |
strictDomLocalRefTypes | Type local refs to DOM elements |
strictContextGenerics | Infer generic component type params |
The Signal APIs
| API | Returns | Introduced |
|---|---|---|
input() | Signal<T | undefined> | v17.1 |
input.required() | Signal<T> | v17.1 |
model() | WritableSignal<T> | v17.2 |
viewChild() | Signal<ElementRef | undefined> | v17.2 |
viewChild.required() | Signal<ElementRef> | v17.2 |
viewChildren() | Signal<readonly T[]> | v17.2 |
contentChild() | Signal<T | undefined> | v17.2 |
The Form Types
| Form Type | Value Type |
|---|---|
FormControl<T> | T | null |
FormControl<T> with nonNullable: true | T |
FormGroup<T> | Partial<T> (disabled controls are absent) |
FormRecord<T> | Record<string, T | null> |
FormArray<T> | T[] |
The HTTP Generic
| Call | Return Type |
|---|---|
http.get<User[]>(url) | Observable<User[]> |
http.get(url, { responseType: 'text' }) | Observable<string> |
http.get(url, { observe: 'response' }) | Observable<HttpResponse<T>> |
http.get<unknown>(url) | Observable<unknown> (safe default) |
Best Practices
✅ Do This:
// Enable strictTemplates in angularCompilerOptions
"strictTemplates": true // ✅
// Use signal inputs with required()
user = input.required<User>(); // ✅
// Use nonNullable for forms that should never be null
new FormControl('', { nonNullable: true }) // ✅
// Use unknown for untrusted API responses
http.get<unknown>(url) // ✅
// Use viewChild.required() when the element is always present
searchInput = viewChild.required<ElementRef>('searchInput'); // ✅
❌ Don’t Do This:
// Don't use any for HTTP responses
http.get<any>(url) // ❌
// Don't use @ViewChild in new code
@ViewChild('input') input!: ElementRef; // ❌ use viewChild()
// Don't leave strictTemplates off
"strictTemplates": false // ❌
// Don't use @Input() for new components
@Input() user!: User; // ❌ use input()
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Template error on nullable input | Library lacks strictNullChecks | Use strictNullInputTypes: false or user! |
$event is any | strictDomEventTypes disabled | Enable it, narrow event.target |
Form value includes null | Control can reset to null | Use nonNullable: true |
Signal query is undefined | Query runs before view init | Access in afterNextRender or effect |
| HTTP response type is wrong | Generic is an assertion, not a check | Validate with assertion function |
Real-World Examples
1. Strict Templates
{ "angularCompilerOptions": { "strictTemplates": true } }
2. Signal Input
user = input.required<User>();
3. Model Input
checked = model(false);
4. Signal Query
searchInput = viewChild.required<ElementRef>('searchInput');
5. Typed Form
new FormControl('', { nonNullable: true })
6. Typed HTTP
this.http.get<User[]>('/api/users')
7. Unknown HTTP
this.http.get<unknown>('/api/users')
8. Event Narrowing
const target = event.target as HTMLInputElement;
9. Generic Component
items = input.required<T[]>();
10. Non-Null Assertion in Template
<user-detail [user]="user!" />
Visual
The Template Type-Checking Flow
┌──────────────────────────────────────────────┐
│ TEMPLATE TYPE CHECKING │
│ │
│ Component class: │
│ user = input.required<User>() │
│ │
│ Template: │
│ [user]="selectedUser" │
│ │
│ Compiler checks: │
│ selectedUser assignable to User? │
│ ├─ YES → no error │
│ └─ NO → compile error │
│ │
│ Enabled by strictTemplates: true │
│ │
└──────────────────────────────────────────────┘
The Signal Input Lifecycle
┌──────────────────────────────────────────────┐
│ SIGNAL INPUT │
│ │
│ input.required<User>() │
│ └─ Signal<User> │
│ └─ Throws if not provided │
│ │
│ Template: │
│ {{ user().name }} │
│ └─ Compiler checks User has `name` │
│ │
│ Parent: │
│ <user-detail [user]="u" /> │
│ └─ u must be assignable to User │
│ │
└──────────────────────────────────────────────┘
The Typed Form Inference
┌──────────────────────────────────────────────┐
│ TYPED FORM │
│ │
│ new FormGroup({ │
│ email: new FormControl('', { │
│ nonNullable: true │
│ }), │
│ password: new FormControl('', { │
│ nonNullable: true │
│ }) │
│ }) │
│ │
│ login.value: { email: string; │
│ password: string } │
│ │
│ No null. No any. Type-safe access. │
│ │
└──────────────────────────────────────────────┘
The HTTP Generic Assertion
┌──────────────────────────────────────────────┐
│ HTTP GENERIC │
│ │
│ http.get<User[]>('/api/users') │
│ └─ Observable<User[]> │
│ └─ Generic is a TYPE ASSERTION │
│ └─ No runtime validation │
│ │
│ For untrusted data: │
│ http.get<unknown>('/api/users') │
│ └─ Observable<unknown> │
│ └─ Validate with assertion function │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Template type checking | strictTemplates: true in angularCompilerOptions |
| Signal inputs | input(), input.required() |
| Signal queries | viewChild(), viewChildren(), contentChild() |
| Model inputs | model() for two-way binding |
| Typed forms | FormControl<T>, FormGroup<T>, nonNullable |
| HTTP generic | http.get<T>() — type assertion, not runtime check |
| Event types | strictDomEventTypes types $event |
| Unknown HTTP | http.get<unknown>() for untrusted data |
| Generic components | input.required<T[]>() |
Key takeaways:
- Angular’s template type checker treats templates as TypeScript code. With
strictTemplates: true, bindings are checked against declared types,$eventhas the correct type, anduser?.nameis typed correctly. The compiler catches template errors before the application runs . - Signal inputs and queries replace decorator APIs.
input(),viewChild(), andmodel()return signals with precise types. The.required()variants throw if the value is missing. Queries areundefineduntil the view or content is initialized . - Typed forms eliminate
anyfrom reactive forms.FormControl<string>enforces that the value isstring | null.nonNullable: trueremovesnull.FormGroupinfers its value type from its controls . - The HTTP generic is an assertion, not a check.
http.get<User[]>()tells the compiler to treat the response asUser[], but does not validate the response shape. For untrusted data, usehttp.get<unknown>()and validate with an assertion function . strictTemplatesenables a suite of granular flags.strictInputTypes,strictNullInputTypes,strictDomEventTypes, and others can be disabled individually when a library’s types are incompatible .- The
$any()function opts out of type checking. When a library’s types are wrong or incomplete,$any(expression)bypasses the check for that expression. It is an escape hatch, not a solution .
Remember: Angular’s TypeScript integration is not a layer added on top. It is the architecture. Components declare their inputs and outputs with types. Templates are checked against those types. Forms infer their value types from their controls. HttpClient accepts a generic that asserts the response shape. The strictTemplates flag enables the full check, and the granular flags let you disable specific checks when a library or edge case requires it. Write types for your inputs, use signal APIs for new components, prefer nonNullable forms, and treat HTTP generics as assertions rather than validations. The compiler catches the rest.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!