feat(dts): type ngAcceptInputType_* with the transform's parameter type - #496
Merged
Merged
Conversation
This was referenced Sep 24, 2026
Brooooooklyn
added this pull request to stack #503
September 30, 2026 04:46
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
September 30, 2026 07:08
1359b77 to
8567a50
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 07:08
add60c9 to
b8fd06a
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
September 30, 2026 13:11
8567a50 to
3f789e6
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 13:11
b8fd06a to
3d138f9
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 15:58
3d138f9 to
bf5288b
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
September 30, 2026 15:58
3f789e6 to
3d2b3b3
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 17:31
bf5288b to
1d53f16
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
September 30, 2026 17:31
3d2b3b3 to
ec3376c
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 18:32
1d53f16 to
56e4adb
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
September 30, 2026 18:32
ec3376c to
3be6c6c
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 19:07
56e4adb to
7ae9dd3
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
September 30, 2026 19:07
3be6c6c to
13db4af
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
September 30, 2026 19:38
7ae9dd3 to
df67666
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
2 times, most recently
from
October 1, 2026 02:43
3d03819 to
0dac7a7
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
October 1, 2026 02:43
df67666 to
023efb2
Compare
Brooooooklyn
previously approved these changes
Oct 1, 2026
This was referenced Oct 1, 2026
Open
Brooooooklyn
removed this pull request from stack #503
October 1, 2026 08:45
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
October 1, 2026 08:46
023efb2 to
2264f94
Compare
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
2 times, most recently
from
October 1, 2026 08:47
0869aa8 to
0d8231d
Compare
Brooooooklyn
force-pushed
the
feat/decorator-metadata-queries
branch
from
October 1, 2026 08:50
2264f94 to
167ea92
Compare
…type Library `.d.ts` declarations wrote `static ngAcceptInputType_<input>: unknown` for every input with a transform, so consumers' template type-checking accepted any value. ngtsc writes the type of the transform's first parameter, so e.g. `[count]="true"` against a `(value: string | number) => number` transform is a type error. That type is now printed the way ngtsc prints it: `@angular/core` names become `i0.Name`, string literals are re-quoted, spacing is normalised, local and global names are kept, `unknown` for a transform with no parameters. Two deliberate differences from ngtsc, both falling back to the previous `unknown`: - a type referencing another module: ngtsc adds `import * as iN` for it, but aliases numbered per source file can't be merged safely into bundled declaration files - an imported transform, whose signature can't be read from one file A member `@Input` overriding an `inputs:` entry decides the type, like the compiled inputs map. Also emits `"ng-component"` as the `.d.ts` selector of a component without one, as ngtsc does (it was `never`).
The `ngAcceptInputType_*` printer copied type parameters, mapped types,
predicates, `this` parameters and non-property members verbatim, so
`@angular/core` names there missed the `i0.` rewrite and other-module names
escaped the `unknown` fallback. Print every node from the AST instead, and
fall back to `unknown` for forms that can't be printed.
Also match TypeScript's printer: property keys and destructuring keys
double-quoted, strings escaped to ASCII (`\0`, `😀`), `{ w }`
patterns, bigint/number normalisation, and the same-line comments it keeps in
front of list elements. `/** */` comments after a type or on their own line are
still dropped.
The new test compares 239 forms against ngtsc 22.1.7's `.d.ts` output.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed local names like ngtsc Checked against @angular/compiler-cli 22.1.7: - An overloaded function or static method transform is typed from its first declaration (the one ngtsc reads), not its implementation. - A qualified type name whose head the file declares (`NS.T`, `C.T` for a class merged with a namespace, `A.B.T`, `declare namespace`) is written as its last part, `T`, as ngtsc writes it. An enum member (`E.A`), which ngtsc can't emit, stays as written; `ng.Signal` through an `@angular/core` namespace is still `i0.Signal`, and names from other modules are still `unknown`. - A TypeScript library function used as a transform (`parseInt`, `isNaN`, ...) is typed with its first parameter's type from `lib.es5.d.ts`. - The ngtsc snapshot gets back the typed `ngAcceptInputType_*` of the 26 cases added while every one was `unknown`, regenerated from ngtsc, plus probes of qualified names and library functions. A static `ngAcceptInputType_*` the class declares itself is TypeScript's own declaration output, so the test doesn't expect oxc to write it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An input whose class property is a string key was written quoted but not escaped whenever it wasn't an identifier, so `'a-"b'` gave `static "ngAcceptInputType_a-"b": ...`, which doesn't parse, and `'c.\\d'` named a different property. ngtsc (22.1.7) quotes the name only when Angular's `isUnsafeObjectKey` (`/[-.]/`) matches, and prints it as a TypeScript string literal (`"ngAcceptInputType_a-\"b"`, `"ngAcceptInputType_k-é"`). Every other name is written as is, like `ngAcceptInputType_a"b`. oxc now does the same, reusing the string printer of the transform types. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…per functions The snapshot's `scope-*` probes (added earlier in the stack, before `.d.ts` transform types) left out ngtsc's `ngAcceptInputType_*` types. With them back, a function expression passed to a helper, as a default value, through `@Input(opts(...))` or written in a helper is typed from its own parameter, like ngtsc 22.1.7 (`string`). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…type names
- An enum member reached through a namespace (`NS.E.A`, `C.E.A` for a
class merged with a namespace, `NS.M.E.A`, `namespace A.B`) is kept as
written, like `E.A`. It was shortened to `A`, which doesn't resolve;
ngtsc 22.1.7 throws on these. `NS.E` and `A.B.T` are still shortened
to `E` / `T`, as ngtsc writes them.
- A computed property name in a transform type (`{ [token]: string }`,
`{ [ns.token]: string }`) follows the type-name rules: an
`@angular/core` value becomes `i0.token`, another module's makes the
type `unknown`. ngtsc copies the expression, which names an import the
`.d.ts` doesn't have. Local and global names are unchanged.
Also compares the `ngAcceptInputType_*` types of the `scope-*` probes added
under #493, and adds a `dts-qualifiedNamespaceEnum` probe.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…spaces and lone surrogates like ngtsc
- `import A = NS` / `import C = NS.T` (and `export import`) stand for their
target: `A.T` and `C` are written `T`, as ngtsc writes them. An alias of
another module (`import R = require('./other')`, or of an import) makes
the type `unknown`, like any other module's type (ngtsc writes the bare
name, which doesn't resolve). An alias of `@angular/core` becomes `i0.X`,
and one of a global is written as its target (`Intl.NumberFormat`).
- `typeof this` is kept, like ngtsc (it was `unknown`).
- A qualified name through an enum merged with a namespace (`E.T`) is
shortened to `T` like ngtsc; only an actual enum member (`E.A`, from any
declaration of the enum) is kept as written.
- Lone surrogates in string literal types and quoted keys (`'\uD800'`) are
printed as `"\uD800"`, like ngtsc, instead of OXC's internal encoding.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r target
`namespace NS { export import Alias = Local; }` with a parameter typed
`NS.Alias` printed `Alias`, where ngtsc resolves the alias and writes `Local`.
The same went for nested namespaces (`NS.Inner.Alias2`), aliases of aliases,
aliases reached through a top-level alias (`import X = NS.Alias`), and targets
named from the namespace's own scope (`export import Rel = In2`).
Namespace member aliases now resolve to their target, whose head is looked up
in the alias's namespace first, then the enclosing ones, then the file. The
result follows the existing rules: local names are shortened, `@angular/core`
names become `i0.X`, other modules make the type `unknown`, and globals keep
their target (`Intl.NumberFormat`).
Also tests that a transform that's a global declared outside the file (`atob`,
now assumed to be a function) is typed `unknown`, like an imported one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…0.X` Every `require` alias was treated as another module's, so `Core.Signal<string>` (also through `import S = Core.Signal`, `import C = Core` or a namespace's alias of it) typed the input as `unknown`. An alias of `@angular/core` now gives `i0.Signal<string>`, like a namespace import of it. ngtsc 22.1.7 writes the bare `Signal<string>`, which doesn't resolve in its `.d.ts`; other modules stay `unknown`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A computed name in a transform's parameter type (`{ [Core.ɵSIGNAL]: string }`)
skipped the alias resolution type names get, so an alias of
`@angular/core` (`import Core = require('@angular/core')`, `import C = ng`,
`import S = Core.ɵSIGNAL`, or one declared in a namespace) was written as
is, another module's alias didn't make the type `unknown`, and a global's
alias wasn't written as its target. Computed names now resolve aliases
the way type names do: `i0.ɵSIGNAL`, `unknown`, and `G.k`. ngtsc 22.1.7
copies all of these as written, which doesn't resolve in its `.d.ts`. An
alias of a name the file declares stays as written, like ngtsc.
The snapshot's `scope-typeOnly-*` probes (added earlier in the stack,
before `.d.ts` transform types) now also compare ngtsc's
`ngAcceptInputType_*` types, including `typeof name` for a helper's
parameter used as a type.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…written
`enum E { [`A`] = 'a' }` names its member with a template literal, which the
enum member check ignored: `E.A` was taken for a qualified local name and
shortened to `A`, which doesn't resolve (`static ngAcceptInputType_x: A;`),
also for an enum in a namespace. Members named `'A'`, `['A']` or `` [`A`] ``
(escapes included) are now recognised like `A`, so the type is kept as
written. ngtsc 22.1.7 throws on all of these.
Transforms declared in a namespace (through `typeof NS.fn`, a static method
of a class there, or `import H = NS.fn`) are typed from their declaration,
like ngtsc; add them to the tests and snapshot.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…line breaks whole An import-equals alias chain is followed until it ends, stopping only on a cycle, instead of giving up after 16 aliases (`A17.T` through 18 aliases is `T`, like ngtsc). Looking back for a `//` before a union now steps over a U+2028 / U+2029 line break by its full UTF-8 length, where it used to slice inside the character and panic. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn
force-pushed
the
feat/dts-input-transform-types
branch
from
October 1, 2026 08:50
0d8231d to
2be0457
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack (4/5): #493 inputs/outputs → #494 transform validation → #495
queries:→ #496.d.tstransform types → #504 signal API import identity. This is #496.Types
static ngAcceptInputType_<input>in library.d.tsoutput with the transform's parameter type, as ngtsc does.The bug
Every input with a transform was declared as
so a consumer's template type-checking accepted any value for it. ngtsc writes the type of the transform's first parameter, so
[count]="true"against(value: string | number) => numberis a type error.The fix
The type is printed the way ngtsc prints it (
directive/dts_type.rs). Every part of the type is printed from the AST, never copied from the source, so type arguments, mapped types, predicates,thisparameters and members all get the same rules:@angular/corenames becomei0.Name(also throughimport * as ng)typeof x, are kept as writtenNS.T,A.B.T) is written as its last part,T, like ngtscé→\u00E9),{ w }patterns, normalised numbers and bigints, and the same-line comments it keeps in front of list elementsunknownThe transform's type comes from:
parseInt,isNaN, ...), typed with its first parameter fromlib.es5.d.ts@Inputoverriding aninputs:entry decides the type, like the compiled inputs mapThe member name is quoted only when it has a
-or.(Angular'sisUnsafeObjectKey) and escaped like a TypeScript string ("ngAcceptInputType_a-\"b"). Any other name is written as is, like ngtsc (ngAcceptInputType_a"b).Deliberate differences from ngtsc:
NS.Tthrough a named import) isunknown. ngtsc adds animport * as iNfor it. These aliases are numbered per source file, which can't be merged safely into bundled declaration files, since the Vite.d.tsinjector combines every module's declarations.unknown: its signature can't be read from one file./** */comments after a type or on their own line, and comments spanning lines, are dropped. ngtsc keeps them.Intl.NumberFormat) is kept as written, where ngtsc writesNumberFormat, which doesn't resolve. An enum member type (E.A) is kept as written, where ngtsc throws.import('...')types areunknown, where ngtsc aborts the build.Known risk: local type names are written as is, like ngtsc. But ngtsc adds these members before bundling, and the Vite plugin injects them after. So in a bundled
.d.tswhere the bundler renamed or dropped that local type, the name can fail to resolve or bind to a different type. Per-file.d.tsoutput is not affected.This PR also emits
"ng-component"as the.d.tsselector of a component without one, as ngtsc does (it wasnever).An enum member reached through a namespace (
NS.E.A,C.E.A) is kept as written, likeE.A(ngtsc throws on these). A computed property name follows the type-name rules: another module's name makes the typeunknown, and@angular/corenames becomei0.X.Import-equals aliases (
import A = NS,import C = NS.T,export import) are typed like their target, as ngtsc does. Aliases of other modules giveunknown, aliases of@angular/coregivei0.X, and aliases of globals give their target (ngtsc writes a bare name that doesn't resolve).typeof thisis kept, a name in a namespace merged with an enum is shortened unless it is a real enum member, and lone surrogates in string types and keys print as"\uD800".Import-equals aliases declared inside namespaces (
NS.Alias,NS.Inner.Alias2, aliases of aliases) are typed as the declaration they resolve to, like ngtsc, with the same core, other-module and global rules as top-level aliases. A transform that's a global declared outside the file is typedunknown.import Core = require('@angular/core')and aliases of it (top level or in a namespace) typeCore.Xasi0.X. ngtsc writes the bareX, which doesn't resolve;requirealiases of other modules stayunknown.A computed property name through an import-equals alias resolves like a type name:
@angular/coregivesi0.X, another module givesunknown, and a global gives its target (ngtsc copies these as written, and its.d.tscan't resolve them). An alias of a name the file declares stays as written, like ngtsc.Enum members named by a string or template literal (
[`A`]) are recognised, soE.A/NS.E.Astay as written instead of becoming an unresolvedA. Transforms declared in namespaces are typed from their declaration, like ngtsc.Import-equals alias chains are followed to their end (a cycle gives
unknown), and a U+2028 / U+2029 line break before a union no longer crashes the.d.tsprinter.Tests
dts_input_transform_type_test.rs(new): compares the printed type of 239 type forms with what ngtsc 22.1.7 wrote into the.d.tsfor the same source. It also covers the documented differences (other-module types, comments,import()types, enum members), an overloaded static method, and member-name quoting and escaping.input transformsspecs fromngtsc_spec, twodirective_specacceptance cases, and probes of the type forms, qualified names and lib functions are added to the ngtsc snapshot. The test maps ngtsc's other-module types tounknown(the documented difference) and doesn't expect astatic ngAcceptInputType_*the class declares itself (TypeScript writes that, not ngtsc). The cases that need declarations from another file, and the qualified-name forms above, are skipped with that reason..d.ts, injected by the Vite plugin's helper, type-checks withskipLibCheck: false. ngtsc withstrictTemplatesaccepts a consumer with valid bindings against it, and rejects wrong ones (booleanfor astring | numbertransform, a value outside a literal union, a missing required input).