fix(spectator): resolve input aliases in props and setInput - #16
Open
nicobytes wants to merge 7 commits into
Open
fix(spectator): resolve input aliases in props and setInput#16nicobytes wants to merge 7 commits into
nicobytes wants to merge 7 commits into
Conversation
Adds the failing cases for openng-org#15 across the Karma, Jest and Vitest suites, before any source change. Neither `props` nor `setInput()` accepts the class property name of an input that declares an alias, even though that is exactly what both types advertise (`props?: InferInputSignals<C>` and `setInput<K extends keyof C>`). Angular resolves inputs by their public name, so the property name reaches `ComponentRef.setInput()` unmapped and is dropped with NG0303 -- silently for optional inputs, and as a thrown NG0950 for `input.required({ alias })`. 8 of the 15 new tests fail: - props + property name, @input('userName') / @input({ alias }) - props + property name, input.required({ alias }) / input(d, { alias }) / model({ alias }) - props mixing property names and alias names - setInput() + property name on an aliased input - createRoutingFactory({ props }) + property name The other 7 pass and stand as regression guards for behaviour that must not change: alias keys keep working, non-aliased inputs are unaffected, an unknown key is still reported rather than redirected, and a key that is both the public name of one input and the property name of another still resolves as the public name. Refs openng-org#15 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ComponentRef.setInput()` addresses inputs by their public name, so an input
declaring an alias was only reachable through that alias. Both `props` and
`setInput()` are typed against the class property names, so the keys the types
advertise were exactly the keys that failed at runtime -- dropped with NG0303,
silently for optional inputs and as a thrown NG0950 for `input.required({ alias })`.
`setProps()` now maps every key through a resolver built from
`reflectComponentType(type).inputs`, which covers decorator inputs, `input()`,
`input.required()` and `model()`:
- a public-name match wins, so existing behaviour is preserved exactly;
- otherwise the property name is mapped to the input's public name;
- an unknown key is passed through untouched, so Angular still reports it
rather than it being silently redirected onto another input.
Public-name precedence is load-bearing: a key can be the public name of one
input and the property name of another, and `props: { age: 1 }` must keep
resolving the way `setInput()` has always resolved it.
`setHostProps()` is deliberately left alone -- it assigns onto the host
component instance, where the class property name is already correct, so
`createHostFactory`, `createDirectiveFactory` and `createPipeFactory` are
unaffected.
`createRoutingFactory` shares `setProps()` and is fixed by the same change.
Refs openng-org#15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`props` was typed `InferInputSignals<C>`, keyed by the class property names, so
addressing an input by its public name was a TS2353 even though it was the only
spelling that worked at runtime before the previous commit.
Adds `InferInputProps<C>` and applies it to `SpectatorOverrides.props`, which
`createRoutingFactory` shares. Aliases cannot be derived from the type system --
neither `@Input('userName')` nor `input({ alias: 'userName' })` carries the
literal into the type -- so an index signature is the only way in.
The form matters. Both simpler candidates were type-checked and rejected:
- `InferInputSignals<C> | Record<string, unknown>` silently drops value-type
checking, letting `props: { name: 123 }` compile on a string input.
- `InferInputSignals<C> & Record<string, unknown>` breaks any `props` object
typed through an `interface` or a class, since those get no implicit index
signature.
The union of the mapped type with its own intersection keeps both: value types
are still checked for every known input, and `interface`, type-alias,
inferred-const and `Record<string, unknown>` sources all stay assignable.
The one thing it can no longer reject is an unknown key in a `props` object
literal, since TypeScript cannot tell an alias from a typo. Angular still
reports that at runtime, which is why the resolver added in the previous commit
passes unknown keys through untouched instead of swallowing them.
`Spectator.setInput()` is left alone: its object overload already accepted
arbitrary public names, so only its runtime behaviour needed fixing.
Refs openng-org#15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The type tests compile against the built `d.ts` bundles, so these also prove `InferInputProps` actually ships for every entry point -- the failure mode of openng-org#8 and openng-org#11. Covers both spellings, and a `props` object typed through an interface, which is the guard against typing `props` as a plain intersection with `Record`. The negative control is the important one: it pins that value types are still checked for known inputs, which is what separates the shipped type from a plain union with `Record<string, unknown>`. Signal inputs rather than decorators, since these tsconfigs are standalone and do not enable `experimentalDecorators`. Refs openng-org#15 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs openng-org#15 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Karma type-checks the specs, and the collision fixture did not compile: `age` is the public name of the `numOfYears` input and also a property of its own, so the type describes the property while the resolver resolves the public name. That divergence is inherent -- the type cannot express a collided name -- so the fixture now gives both inputs the same value type. The test still pins that the key resolves as a public name, which is the behaviour that has to stay stable. Refs openng-org#15 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes Spectator’s props and setInput() behavior for Angular inputs that use public-name aliases (including decorator inputs, signal input()/input.required(), and model()), by resolving provided keys to the public input name that ComponentRef.setInput() expects. It also widens the props typing to accept alias keys while preserving strong typing for known inputs, and adds tests + docs to lock in the behavior.
Changes:
- Add a runtime resolver that maps class property names to Angular’s public input names before calling
ComponentRef.setInput(). - Update
propstyping to allow alias keys without losing value-type checking for known inputs. - Add cross-runner tests (jasmine/jest/vitest), type tests, and documentation updates covering the new behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| type-tests/vitest/consumer.ts | Adds type-level coverage demonstrating props accepts both property names and alias keys while keeping known input value types checked. |
| type-tests/jest/consumer.ts | Same type-level coverage for the Jest entry point. |
| type-tests/jasmine/consumer.ts | Same type-level coverage for the Jasmine/default entry point. |
| projects/spectator/vitest/test/props-alias-names.spec.ts | Adds Vitest runtime tests for props/setInput() with aliased inputs, collisions, and unknown keys. |
| projects/spectator/jest/test/props-alias-names.spec.ts | Adds Jest runtime tests mirroring the Vitest suite. |
| projects/spectator/test/props-alias-names.spec.ts | Adds Jasmine/Karma runtime tests mirroring the Vitest suite. |
| projects/spectator/src/lib/types.ts | Introduces InferInputProps<C> to widen accepted props keys (including aliases). |
| projects/spectator/src/lib/spectator/create-factory.ts | Switches props typing in createComponentFactory pipeline to InferInputProps. |
| projects/spectator/src/lib/spectator-routing/create-factory.ts | Switches routing factory props typing to InferInputProps. |
| projects/spectator/src/lib/internals/resolve-input-name.ts | New internal helper building an input-name resolver via reflectComponentType(...).inputs. |
| projects/spectator/src/lib/internals/query.ts | Uses the resolver inside setProps() to normalize keys before delegating to Angular. |
| docs/src/content/docs/testing-components.md | Documents that props and setInput() accept both property names and public input names (aliases), and clarifies compile-time vs runtime validation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review raised the concern that `setProps()` would throw on `props: undefined`, since the non-string branch hands `keyOrKeyValues` straight to `for..in`. It does not -- `for..in` over `null` or `undefined` is a spec-mandated no-op, not a throw -- so no guard is added. These tests pin it instead, across the three reachable shapes: explicit `props: undefined`, no overrides at all, and `setInput()` given nothing. Refs openng-org#15 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zJaaal
approved these changes
Aug 26, 2026
Author
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.
Fixes #15.
Problem
propsandsetInput()both funnel intosetProps(), which callsComponentRef.setInput(key, value). Angular resolves inputs by their public name (the alias). But both public APIs are typed by the class property name:props?: InferInputSignals<C>— keys arekeyof CsetInput<K extends keyof C>(input: K, ...)So the keys TypeScript advertises are exactly the keys that fail at runtime. This is wider than the issue title: neither API accepts the class property name.
setInput('userName', v)— aliassetInput('name', v)— property namekeyof Coverload)NG0303, silently ignoredprops: { name: v }— property nameNG0303, silently ignoredprops: { userName: v }— aliasTS2353props: { name: v }+input.required({ alias })NG0950The silent rows are the dangerous ones — the test just renders empty, with no obvious cause.
Fix
1. Runtime — resolve the key before handing it to Angular
A new
internals/resolve-input-name.tsbuilds a resolver fromreflectComponentType(type).inputs({ propName, templateName, isSignal }, which covers decorator inputs,input(),input.required()andmodel()), andsetProps()maps every key through it:propName → templateName;NG0303).Public-name precedence is required, not cosmetic. A key can be the
templateNameof one input and thepropNameof another:props: { age: 1 }setsnumOfYearstoday, and still does. There is a test pinning it.setHostProps()is deliberately not touched — it assigns onto the host component instance, where the class property name is already correct. SocreateHostFactory/createDirectiveFactory/createPipeFactoryare unaffected.2. Types — let alias keys through without loosening what matters
Aliases cannot be derived from the type system: neither
@Input('userName')norinput.required({ alias: 'userName' })encodes the literal in the type. An index signature is the only way in. I type-checked the candidates withtsc --strictrather than guessing:propsobjectInferInputSignals<C>(status quo)TS2353… | Record<string, unknown>… & Record<string, unknown>interface/class-typedT | (T & Record<string, unknown>)The plain union lets
props: { name: 123 }compile clean on a string input. The plain intersection is a breaking change for a common pattern, since interfaces get no implicit index signature:So this PR ships the last row:
Verified against the real Angular types: alias keys accepted;
interface, type-alias and inferred-const sources still assignable; and{ name: 999 },{ sigReq: 'nope' },{ sigOpt: input(5) },{ mdl: 42 }all still rejected.Accepted trade-off, stated plainly: an unknown key in a
propsobject literal is no longer a compile error. That is unavoidable — TypeScript cannot tell an alias from a typo. RuntimeNG0303remains the guard, which is exactly why the resolver passes unknown keys through instead of swallowing them.The runtime fix and the type change are separate commits so the type trade-off can be reviewed, or rejected, without losing the runtime fix. The runtime fix alone already closes the issue's core complaint, since it makes the property name — what the type already advertises — actually work.
Result
Known limitation
When a key is the public name of one input and a property of another, the type
describes the property while the resolver resolves the public name. So a collision
is only expressible in
propswhen the two inputs agree on their value type:The runtime side is unchanged from today and there is a test pinning it. Nothing to
do about the type: it cannot describe two inputs sharing one name. Worth knowing,
not worth blocking on — Karma surfaced it while type-checking the specs.
Verification
yarn test(Karma)yarn test:jestyarn test:vitestyarn build && yarn test:typesyarn lintmain(verified by comparison)Tests
New
props-alias-names.spec.ts, triplicated across the three suites (identical but for the import specifier), mirroring the layout of the existingset-input-alias-names.spec.ts. 15 cases:props+ property name for@Input('x'),@Input({alias}),input.required({alias}),input(d, {alias}),model({alias})props+ alias names (regression guard)propsmixing both spellings in one objectsetInput()+ property name on an aliased inputcreateRoutingFactory({ props }), both spellingsAt the tests-first commit 8 fail with
NG0303/NG0950and 7 pass as regression guards, so the RED phase is visible in the history.🤖 Generated with Claude Code