Skip to content

fix(spectator): resolve input aliases in props and setInput - #16

Merged
dominicbachmann merged 7 commits into
openng-org:mainfrom
nicobytes:fix/props-input-aliases
Sep 25, 2026
Merged

dominicbachmann merged 7 commits into
openng-org:mainfrom
nicobytes:fix/props-input-aliases

Conversation

@nicobytes

@nicobytes nicobytes commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #15.

Problem

props and setInput() both funnel into setProps(), which calls ComponentRef.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 are keyof C
  • setInput<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.

Call TypeScript Runtime
setInput('userName', v) — alias OK works
setInput('name', v) — property name OK (keyof C overload) NG0303, silently ignored
props: { name: v } — property name OK NG0303, silently ignored
props: { userName: v } — alias TS2353 works
props: { name: v } + input.required({ alias }) OK throws NG0950

The 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.ts builds a resolver from reflectComponentType(type).inputs ({ propName, templateName, isSignal }, which covers decorator inputs, input(), input.required() and model()), and setProps() maps every key through it:

  • a public name match wins, so existing behaviour is preserved exactly;
  • otherwise fall back to propName → templateName;
  • an unknown key is passed through untouched, so Angular still reports it (NG0303).

Public-name precedence is required, not cosmetic. A key can be the templateName of one input and the propName of another:

class C {
  @Input({ alias: 'age' }) numOfYears = 0;  // templateName 'age'
  @Input({ alias: 'collided' }) age = '';   // propName  'age'
}

props: { age: 1 } sets numOfYears today, 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. So createHostFactory / createDirectiveFactory / createPipeFactory are unaffected.

2. Types — let alias keys through without loosening what matters

Aliases cannot be derived from the type system: neither @Input('userName') nor input.required({ alias: 'userName' }) encodes the literal in the type. An index signature is the only way in. I type-checked the candidates with tsc --strict rather than guessing:

Candidate alias keys value types on known inputs pre-declared props object
InferInputSignals<C> (status quo) ❌ TS2353 ✅ enforced ✅ assignable
… | Record<string, unknown> ✅ ❌ lost ✅
… & Record<string, unknown> ✅ ✅ ❌ breaks interface/class-typed
T | (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:

interface MyProps { name: string }
const p: MyProps = { name: 'John' };
createComponent({ props: p });
// TS2322: Index signature for type 'string' is missing in type 'MyProps'

So this PR ships the last row:

export type InferInputProps<C> = InferInputSignals<C> | (InferInputSignals<C> & Record<string, unknown>);

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 props object literal is no longer a compile error. That is unavoidable — TypeScript cannot tell an alias from a typo. Runtime NG0303 remains 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

@Component({ /* … */ })
class UserComponent {
  @Input('userName') name = '';
  age = input.required<number>({ alias: 'userAge' });
}

// before: NG0303 (silent) / NG0950 (throw) -> had to createComponent() then setInput()
// after: both spellings work
createComponent({ props: { name: 'John', age: 30 } });        // class property names
createComponent({ props: { userName: 'John', userAge: 30 } }); // public aliases

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 props when the two inputs agree on their value type:

class C {
  @Input({ alias: 'age' }) numOfYears = 0;   // public name 'age', number
  @Input({ alias: 'collided' }) age = '';    // property   'age', string
}

createComponent({ props: { age: 1 } });
// TS2322: type 'number' is not assignable to type 'string'
// — the type sees the `age` property; the runtime sets `numOfYears`

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) 341/341 — this one type-checks the specs, so it also validates the type change under real compilation
yarn test:jest 47 suites, 257 tests
yarn test:vitest 47 files, 227 tests
yarn build && yarn test:types clean for all three entry points
yarn lint 0 errors; the 3 warnings are pre-existing on main (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 existing set-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)
  • props mixing both spellings in one object
  • setInput() + property name on an aliased input
  • createRoutingFactory({ props }), both spellings
  • inputs with no alias, unaffected
  • an unknown key is not redirected onto another input
  • the public-name/property-name collision above

At the tests-first commit 8 fail with NG0303 / NG0950 and 7 pass as regression guards, so the RED phase is visible in the history.

Summary by CodeRabbit

  • New Features
    • Component and routing factory props options and setInput now accept either an input’s class property name or its public alias, including for signal and model inputs.
    • Type checking supports both naming options while continuing to enforce input value types.
  • Documentation
    • Clarified input naming behavior with examples for factory props and setInput.

@nicobytes
nicobytes marked this pull request as ready for review August 26, 2026 14:36
Copilot AI lite review requested due to automatic review settings August 26, 2026 14:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 props typing 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.

Comment thread projects/spectator/src/lib/internals/query.ts
@nicobytes

Copy link
Copy Markdown
Contributor Author

@dominicbachmann 🙏

@nicobytes

Copy link
Copy Markdown
Contributor Author

Hi @dominicbachmann 👋 — just checking in on this one. It's approved, all checks are green and it's mergeable. Is there anything else needed on my side before this can be merged?

Thanks!

@dominicbachmann

Copy link
Copy Markdown
Member

Hi @dominicbachmann 👋 — just checking in on this one. It's approved, all checks are green and it's mergeable. Is there anything else needed on my side before this can be merged?

Thanks!

Can you please remove the co-authored from the commit messages

@nicobytes
nicobytes force-pushed the fix/props-input-aliases branch from 2f64237 to 124fa69 Compare September 23, 2026 14:21
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 76a82989-d73e-4945-ba5e-9b5d65885a6c

📥 Commits

Reviewing files that changed from the base of the PR and between 28bc244 and 154c2ae.

📒 Files selected for processing (12)
  • docs/src/content/docs/testing-components.md
  • projects/spectator/jest/test/props-alias-names.spec.ts
  • projects/spectator/src/lib/internals/query.ts
  • projects/spectator/src/lib/internals/resolve-input-name.ts
  • projects/spectator/src/lib/spectator-routing/create-factory.ts
  • projects/spectator/src/lib/spectator/create-factory.ts
  • projects/spectator/src/lib/types.ts
  • projects/spectator/test/props-alias-names.spec.ts
  • projects/spectator/vitest/test/props-alias-names.spec.ts
  • type-tests/jasmine/consumer.ts
  • type-tests/jest/consumer.ts
  • type-tests/vitest/consumer.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Factory props now accept Angular input property names and public aliases. Runtime handling resolves property names before calling setInput. Tests and documentation cover component and routing factories.

Changes

Aliased input props

Layer / File(s) Summary
Props types and factory signatures
projects/spectator/src/lib/types.ts, projects/spectator/src/lib/spectator/create-factory.ts, projects/spectator/src/lib/spectator-routing/create-factory.ts, type-tests/*/consumer.ts
Adds InferInputProps to accept input property names and aliases. Component and routing factory props use the new type. Type tests check both key forms, interface-typed props, and known input value types.
Runtime input name resolution
projects/spectator/src/lib/internals/resolve-input-name.ts, projects/spectator/src/lib/internals/query.ts, projects/spectator/{test,jest/test,vitest/test}/props-alias-names.spec.ts, docs/src/content/docs/testing-components.md
setProps maps property names to public input names and leaves public names and unknown keys unchanged. Tests cover decorators, signal inputs, models, collisions, unknown keys, no-op cases, and routing factories. The documentation describes both accepted key forms.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: dominicbachmann, zjaaal

Merge Risk: ⚪ Minimal · up to 154c2

Component and routing factory props, and setInput(), now accept both an input's class property name and its public alias. Unknown keys still fall through to Angular's runtime error. The trade-off that unknown object-literal keys are no longer compile-time errors is intentional and documented. No concrete defects were found, and the change appears ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 11 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue [#15] requires props to accept class property names and public aliases, resolve property names before Angular setInput(), preserve public-name precedence, and pass unknown keys through. `pro…
Out of Scope Changes check ✅ Passed The changes stay within issue [#15]. Runtime resolution, type changes, type tests, framework test coverage, routing coverage, and documentation directly support aliased component inputs. `setHostProps…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: resolving Angular input aliases in props and setInput.
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 11 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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
`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
`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
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
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
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
@nicobytes
nicobytes force-pushed the fix/props-input-aliases branch from 124fa69 to 154c2ae Compare September 23, 2026 14:24
@nicobytes

Copy link
Copy Markdown
Contributor Author

Hi @dominicbachmann 👋 — just checking in on this one. It's approved, all checks are green and it's mergeable. Is there anything else needed on my side before this can be merged?
Thanks!

Can you please remove the co-authored from the commit messages

Done!! @dominicbachmann

@dominicbachmann
dominicbachmann merged commit 12fbbeb into openng-org:main Sep 25, 2026
1 check passed
@dominicbachmann

Copy link
Copy Markdown
Member

@nicobytes Thank you for your contributions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: createComponent({ props }) does not support input aliases

4 participants