Angular 20: What's New?

New Requirements: TypeScript v5.8 and Node v20

Angular v20 is not ambiguous about its new requirements. You must now be on TypeScript v5.8 (which, by the way, was already supported since v19.2), so it's time to say goodbye to older TypeScript versions.

And for Node, welcome Node v20 as the new minimum. Angular v19 was the farewell tour for Node 18. Time to update those environments, folks!

2025 Style Guide: A Breath of Fresh Air!

The Angular Style Guide has received a major overhaul! Many recommendations have been trimmed down to focus on what really matters.

  • Trimmed Filenames: Forget UserComponent in user.component.ts. It's now just User in user.ts. Same for directives, pipes, etc. The CLI is already enforcing this for new things. How long will it take for your muscle memory to adapt?
  • Lighter Folder Structure: The top-level app folder might disappear soon (though the CLI hasn't gotten there yet).
  • Property Visibility: protected is now the way for properties used only in your template, and readonly for all those Angular-initialized properties (input(), output(), etc.). It just makes sense!
  • Class and Style Bindings: It's official! [class.something] and [style.something] are the recommended champions over ngClass and ngStyle.

This is a big shift. New projects will adopt it by default. Existing ones? Well, you either migrate or stick to the old ways (the CLI helps with a setting for that, phew!).

Signal APIs: Mostly Greenlit and Stable!

Signals are the future, and the APIs are solidifying! Most are now stable:

  • effect()
  • toSignal()
  • toObservable()
  • afterRenderEffect()
  • afterNextRender()
  • linkedSignal()
  • PendingTasks

Heads up! afterRender() has been renamed to afterEveryRender() and is stable. Crucially, the old name is gone with no automatic migration. Oof, that could sting!

Also, TestBed.flushEffects() (that slightly elusive developer preview API) is deprecated. Use TestBed.tick() now, which runs the full sync process, much closer to real app behavior. effect() lost its forceRoot option (was anyone using it much?), and toSignal() dropped rejectErrors (good call, best practices for the win!). pendingUntilEvent() is still warming up in developer preview.

Zoneless: Out of Experimental, Into Developer Preview!

If signals are the future of reactivity, Zoneless is the future of change detection! It's no longer "experimental," it's now officially in developer preview.

  • provideExperimentalZonelessChangeDetection is now just provideZonelessChangeDetection.
  • The CLI flag --experimental-zoneless is now just --zoneless.

The CLI will even ask if you want to enable it for new projects. Ready for #NoZone?

What's Leaving, What's Staying, and What Might Break Your Code:

As with any major version, some things are getting tossed:

  • The ngIf, ngFor, ngSwitch directives are officially deprecated. The built-in control flow (@if, @for, @switch) is king now. Start migrating; they'll likely be gone in v22! ng update will lend a hand.
  • fixture.autoDetectChanges(boolean): The boolean parameter is gone. Just use fixture.autoDetectChanges(). Using fixture.autoDetectChanges(false) in a zoneless test? That will now throw an error.
  • TestBed.get(): Finally, finally GONE! It was deprecated way back in Angular v9. TestBed.inject() is your friend (and has been for a while). An automatic migration will handle this.
  • InjectFlags Enum: Removed. Option objects for DI APIs (like inject()) have been the norm since v14.1.
  • DOCUMENT Token: Moved from @angular/common to @angular/core. A migration will update your imports.
  • @angular/platform-browser-dynamic: Deprecated in favor of @angular/platform-browser. You'll have to manually update imports for this one for now.
  • @angular/platform-server/testing: Also deprecated, with no replacement. E2E tests are now the recommended way to verify SSR apps.
  • HammerJS Integration: Deprecated. HammerJS hasn't seen an update in 8 years. It's time to say goodbye to those framework entities.
  • ng-reflect-* Attributes: Gone by default in dev mode. They were for old devtools. If you relied on them (you probably shouldn't have!), you can re-enable them with provideNgReflectAttributes(). But maybe... just don't?

Templates: New Tricks Up Their Sleeve!

Your templates just got a bit more expressive:

  • Exponentiation: {{ 2 ** 3 }} is now possible. Makes calculations easier!
  • Tagged Template Literals: Yep, {{ translate\app.title` }}` is here (technically since v19.2, but it's settled now).
  • void Operator: Use it like <button (click)="void selectUser()"> to explicitly ignore a function's return, especially useful for event listeners where return false might prevent default behavior.
  • in Operator: Check for properties like @if ('invoicing' in permissions). Super useful!

Extended Diagnostics: The Compiler Keeps You in the Loop!

More built-in checks to catch common errors:

  • missingStructuralDirective: Using something like *ngTemplateOutlet but forgot to import NgTemplateOutlet? The compiler will now let you know (with strictTemplates).
  • uninvokedTrackFunction: Wrote @for (user of users; track getUserId) instead of track getUserId(user)? You'll get a friendly nudge.
  • unparenthesizedNullishCoalescing: Mixing ?? with && or || (e.g., a ?? b && c) now requires parentheses for clarity, like {{ a ?? (b && c) }}. TypeScript does this, so it's great to see in templates too!

You can suppress these in tsconfig.json if you really want to, but... why would you?

Host Binding Type Checking: No More Mysteries!

There's a new compiler option typeCheckHostBindings (already in new CLI projects). If you use host metadata in decorators (or @HostBinding/@HostListener), the compiler now checks:

  1. If the binding/listener target (e.g., value in [value]="value()") is actually valid for the host element.
  2. If the property in your component/directive class (e.g., value()) actually exists.

This is huge for catching typos and ensuring your host bindings are legit!

Error Handling: Fewer Errors Slipping Through!

  • provideBrowserGlobalErrorListeners: A new provider (added by default in new CLI projects) to register global error listeners in the browser. Catches errors Angular might otherwise miss.
  • Errors in event listeners are now reported to Angular's internal error handler. This means you might see errors in tests that were previously silent. Time to fix them (or use rethrowApplicationErrors: false in configureTestingModule as a last resort).

Dynamically Created Components: Level Up!

createComponent() (and ViewContainerRef.createComponent) got way cooler in v20! You can now pass options to:

  • Specify directives to apply to the dynamic component.
  • Provide input values using the new inputBinding() function.
  • Declare two-way bindings with twoWayBinding().
  • Listen to outputs with outputBinding().

This is a huge improvement over calling setInput after the first change detection. More power and control! Will this come to TestBed.createComponent() too?

Forms: Still Waiting on Signals, but...

No signal-based forms yet (we're all on the edge of our seats for that!). But v20 brings a couple of small but welcome tweaks:

  • userForm.resetForm(undefined, { emitEvent: false }): Reset forms without triggering events.
  • markAllAsDirty(): Finally, a method on AbstractControl to mark a control and all its descendants as dirty. markAllAsTouched() was missing a sibling!

Router: Smoother Navigation Ahead!

Some cool improvements for the router:

  • Scroll Options: Pass native scroll options to ViewportScroller.scrollToAnchor()/scrollToPosition(). For example, behavior: 'smooth' for that slick scrolling effect.
  • Resolvers with More Context: Child route resolvers can now access resolved data from their parent route! route.data.user from the parent will be available. Less gymnastics to get data!
  • Async Redirects: The redirectTo option in route configs can now accept a function that returns a Promise or an Observable for async redirects. (Technically a breaking change due to the evolving return type).
  • Custom Element Support: Writing Web Components? You can now use a custom element as the host of a RouterLink.

Http: Evolving resource APIs and keepalive!

  • resource API Changes: The query parameter of resource() is now params. For rxResource(), loader is now stream. The reload method moved to WritableResource (only mutable resources can be reloaded).
  • httpResource Updates: The map option is now parse. You can specify the HTTP context in the options. And, the request must now be reactive (e.g., httpResource<User[]>(() => '/users') instead of just the URL string).
  • keepalive Support: HttpClient now supports the keepalive option when using the Fetch API (enabled with withFetch()). Requests won't be aborted if the page unloads. Useful for things like analytics.

Profiling: See Where Your App's Performance is Hit!

There's a new enableProfiling() function in @angular/core. Call this, and Angular will use the browser's Performance API to label framework operations (change detection, templates, outputs, defer, etc.). Then, open Chrome Devtools, record a performance profile, and see the custom "Angular" track. Finally, a clear view of internal performance!

Devtools: Better Insights!

Angular Devtools are getting smarter:

  • OnPush components are now marked as such in the component tree.
  • Deferred blocks (defer) are also displayed.
  • Signal support is improving – we should see the signal tree soon! Peeking under the hood is now easier.

SSR: Stable APIs and Streamlined Config!

Good news for Server-Side Rendering:

  • The withI18nSupport() and withIncrementalHydration() APIs are now stable!
  • provideServerRendering() (now in @angular/ssr instead of @angular/platform-server) is combined with provideServerRoutesConfig() into a single provideServerRendering(withRoutes(serverRoutes)). A migration will handle this.
  • New CLI apps with --ssr get Express v5 and server routing support by default (the --server-routing option is gone).

Angular CLI: This is HUGE!

The CLI saw a ton of changes. Brace yourselves:

  • Updated Naming for 2025 Style Guide: As mentioned, user.ts (with class User) instead of user.component.ts (class UserComponent). Same for directives, services. Pipes, resolvers, etc. use hyphens in filenames (from-now-pipe.ts). A migration for angular.json configures your existing projects to use the old convention if you want a smooth transition.
  • TypeScript Config: The module option is now preserve (better reflecting modern bundlers). tsconfig.json uses a "solution" style, referencing tsconfig.app.json and tsconfig.spec.json.
  • Simplified angular.json: New projects use @angular/build directly, dropping @angular-devkit/build-angular and its transitive Webpack dependencies. That's almost 200 Mb less in node_modules! Some options like outputPath are also removed as they have sensible defaults. Lighter and more powerful!
  • Browserslist Config: Now targets the "widely available" base (browsers released < 30 months from the main Baseline set). More consistent and realistic browser support.
  • Sass Package Importers: You can now use pkg: importers, like @use 'pkg:@angular/material' as mat;.
  • Ahead of Time (AoT) Testing and Template Code Coverage: Add "aot": true to your test options in angular.json. Run tests in the same mode as production! Plus, you get code coverage for templates.
  • Testing with Vitest (Experimental!): Big news! The CLI now supports running tests with Vitest. There's a new @angular/build:unit-test builder. Karma and Jasmine might have a new challenger.
    • Set it up with "runner": "vitest". You'll need a "buildTarget".
    • Defaults to Node with jsdom (install it).
    • Update tsconfig.spec.json types to ["vitest/globals"].
    • Browser Mode: Yep! "browsers": ["chromium"] (or firefox, webkit). Uses Playwright or WebdriverIO (install one).
    • Watch mode is faster (only runs affected tests). Full runs might be a bit slower than Karma.
    • --no-watch is often implied in CI.
    • New --debug flag (Vitest + jsdom/Playwright).
    • Limitations: No custom Vitest config file yet. But the providersFile option lets you set up things like zoneless testing. Reporter and coverage exclusions are in angular.json.
  • Automatic Chrome Workspace Folders: The Vite dev server now helps Chrome Devtools map your project files, allowing direct editing from Devtools that saves to your disk! Enable it in Chrome flags.
  • Sourcemaps without Sources: Generate sourcemaps without embedding the original source code ("sourcesContent": false). Great for production error reporting without exposing all your code.

Angular and its Future with AI!

Angular is positioning itself to facilitate the development of applications with generative AI capabilities:

File: llms.txt An llms.txt file is maintained in the Angular repository. This file helps large language models (LLMs) discover the latest and most correct Angular documentation and code examples, so they generate more modern and accurate code, avoiding issues with obsolete APIs or syntax.

Guides and Resources: Guides and resources are being provided, angular.dev/ai, including examples and live streams showing how to integrate Angular with tools like Genkit and Vertex AI from Google Cloud.

What We Might See in Angular v21

  • Selectorless Components: Imagine this:
    import { User } from './user/user'; // TS import is still needed @Component({ template: '<User [name]="name()" (selected)="selectUser()" />', // NO Angular 'imports' array needed for User! }) export class App { /* ... */ }
    Components used by their class name directly in templates! Directives might use an @ prefix (e.g., <User @CdkDrag />). Pipes also by class name ({{ date | FromNowPipe }}). The syntax is FAR from final, but the compiler work has begun. Mind-blowing, right? This could be revolutionary! An RFC should be coming.
  • Signal-based Forms (The Third Way?): Get ready for a potential new way of doing forms, separate from template-driven and reactive!
    // VERY EARLY PROTOTYPE - DO NOT USE YET! @Component({ selector: "user-form", imports: [FieldDirective], // New directive template: ` <form> <label>Username: <input [field]="userForm.username" /></label> <label>Name: <input [field]="userForm.name" /></label> </form> `, }) class UserFormComponent { userModel = signal<UserModel>({ /* ... */ }); protected readonly userForm: Field<User> = form<User>( // new form() function userModel, // data to edit (userPath: FieldPath<User>) => { // schema for dynamic behavior and validation disabled(userPath.username, () => true, "Cannot change username"); required(userPath.name); error(userPath.age, ({ value }) => value() < 18, "Must be over 18"); } ); }
    A new form() function and Field class to handle form state with signals. Check out the design doc if you're curious, or wait for the RFC!