skip to content

What is the Angular Ivy compiler's locality principle, and why does it require decorator metadata to be statically analyzable?

level: middleimportance: nice to knowfreq 24%

answer

  1. one class at a time
  2. dependencies known by their declarations
  3. no code runs at compile time
  4. object literal in the decorator

basics

~20 s

Locality means the Angular compiler builds each class's definition from its own decorator plus the declared shape of what it uses. Since no code runs at build time, decorator arguments must be object literals the compiler can evaluate statically.

solid answer

~40 s

Under Ivy's **locality** principle, the compiler generates `ɵcmp` for a component from that component's own decorator and the **declared shape** of its dependencies - their selector, inputs, outputs and standalone flag - which for a library comes from the `.d.ts` field `static ɵcmp: ɵɵComponentDeclaration<...>`. It never needs a dependency's template or implementation, which enables incremental rebuilds and precompiled libraries. The catch is that the compiler must *read* metadata without *executing* it. So the decorator argument must be an object literal (otherwise `NG1001`, "argument must be an object literal"), and each value must be evaluable by the compiler's static evaluator: literals, exported or module-level constants, imported references, and functions whose body is a single `return`. Anything that depends on runtime values fails with `NG1010`, "Value could not be determined statically".

code

ts · 12 lines
ts
import { Component } from '@angular/core';

const config = { selector: 'app-badge', template: '<span>new</span>' };

// NG1001: @Component argument must be an object literal
@Component(config)
export class Badge {}

// Compiles: the literal is visible, and the constant is resolved statically
const BADGE_TPL = '<span>new</span>';
@Component({ selector: 'app-badge-ok', template: BADGE_TPL })
export class BadgeOk {}

go deeper

for a junior

Remember that the @Component argument must be written as an object literal and that its values must be known at build time.

for a middle

Explain locality as compiling each class from its own decorator plus dependency summaries, and connect it to NG1001 and the static evaluation limits.

for a senior

Diagnose NG1010 by tracing the dynamic value the compiler points at, and explain how .d.ts declarations let libraries be consumed without their source.

for a principal

Relate locality to build-time scaling in a large workspace: standalone imports keep information local, while NgModule scopes force global recomputation.

## Locality in one sentence The Ivy compiler compiles **one decorated class at a time**, using the class's own decorator plus a *summary* of each class it references, never the full source of the whole application. This design goal is called **locality**. ## What the compiler needs about a dependency When `Parent` uses `<app-child>` in its template, the compiler has to know facts about `Child`: - its **selector**, to know that `<app-child>` matches it; - its **inputs and outputs**, to check and wire up bindings; - whether it is **standalone**, its `exportAs` name, and a few similar facts. It does *not* need `Child`'s template, styles or method bodies. The generated code for `Parent` only *references* `Child` (in `ɵcmp.dependencies`), and at runtime the view reads `Child.ɵcmp` for the rest. For a dependency that comes from a compiled library, that summary lives in the type declaration file. A compiled library's `.d.ts` contains lines such as: ```ts static ɵcmp: i0.ɵɵComponentDeclaration<ChildCmp, "app-child", never, { "label": { "alias": "label"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>; ``` The TypeScript types themselves carry the selector, inputs and outputs, so the consuming compiler can compile a parent against a library without the library's source. ## What locality buys | Benefit | Why locality enables it | | :-- | :-- | | Incremental rebuilds | The compiler tracks per-file *local* information and can reuse it when unrelated files change | | Precompiled libraries | A library is compiled once; consumers only need its `.d.ts` summaries | | Tree-shaking | A component references exactly its dependencies, so unused ones can be dropped | | Simpler runtime | Each definition is self-contained and read directly from a static field | The compiler's own design notes separate **local information** (component and directive metadata) from **global information** (NgModule scopes, which have to be recomputed across files). Standalone components with an explicit `imports` array keep almost everything local; NgModule-declared components still rely on module scope, which is computed across files. ## Why metadata must be statically analyzable To stay local and fast, the compiler must **read** metadata from the TypeScript syntax tree without **running** the program. That produces a set of rules: 1. **The decorator argument must be an object literal.** `@Component(sharedConfig)` fails with `NG1001` ("argument must be an object literal"): Ivy compiles decorators by moving each property's expression into generated definitions, so it needs to see the properties directly. 2. **Each value must be statically evaluable.** The compiler's static evaluator understands literals, references to constants (in the same file or imported), property access, spreads of known arrays, simple operators and calls to functions whose body is a single `return` statement. 3. **Runtime values fail.** A selector built from `window.location`, a `template` read from a file at runtime, or a function with loops or several statements cannot be resolved; the compiler reports `NG1010` with "Value could not be determined statically" and points at the dynamic part. 4. **Bindable members must be visible.** Signal inputs created with `input()` may be public or protected but not private; the compiler rejects a private one. ## A worked example ```ts export const SHARED = [DatePipe, HighlightDirective]; // statically known array const TPL = '<p>{{ when() | date }}</p>'; // module-level constant @Component({ selector: 'app-when', imports: [...SHARED], // OK: spread of a known array template: TPL, // OK: folded from a constant }) export class When { when = input.required<Date>(); } ``` The compiler resolves `SHARED` and `TPL` by following the references in the syntax tree. If `TPL` were instead `loadTemplate()`, where `loadTemplate` builds a string in a loop, the value could not be evaluated and the build would fail. ## Interview framing - Locality is **why** the rules exist; the rules are not arbitrary style constraints. - The `.d.ts` summary is **how** locality reaches across package boundaries. - Strict object-literal decorators were an intentional Ivy change: Ivy compiles decorators by moving their expressions into the generated definitions, so it must see each property directly. - A good answer names both error codes: `NG1001` for a non-literal argument and `NG1010` for a value that cannot be determined statically, and says how to fix each (inline the literal; replace the runtime value with a constant or a provider).

  • How can an Angular app compile a parent component against a library's child component without the library's source?
    The library's `.d.ts` declares `static ɵcmp: ɵɵComponentDeclaration<...>` whose type parameters encode the selector, inputs, outputs and standalone flag. The app's compiler reads those types to match `<lib-child>` and check bindings, and the generated parent code only references the child class, whose definition is used at runtime.
  • Can an Angular @Component use a helper function to compute part of its metadata?
    Yes, if the compiler can evaluate the call statically: the function must be visible to the compiler and its body must be a single `return` of an evaluable expression. A function with several statements, loops or runtime inputs produces a dynamic value, and the build fails with "Value could not be determined statically".

saying these in an interview costs you the question

  • The compiler needs every dependency's template source to compile a component.
  • Any JavaScript expression is allowed in decorator metadata because the build runs the code.
  • Passing a config variable to @Component works as long as the variable is const.
  • An input() signal can be declared private since the template is inside the class.
  • Locality means each component is compiled into its own separate bundle file.