Skip to content

Conversation

@tamb
Copy link
Contributor

@tamb tamb commented Nov 17, 2025

Add resolveVar function for compile-time variable resolution in @listen and @event decorators

What is the current behavior?

Currently, the @Listen and @Event decorators only accept string literals as event names. This forces developers to hardcode event names as strings, which leads to:

  • Typos: No compile-time checking for event name typos
  • Refactoring difficulties: When renaming events, developers must manually find and update all string literals
  • No type safety: Event names are not type-checked, making it easy to use incorrect event names
  • Poor maintainability: Event names scattered as magic strings throughout the codebase

Example of current limitation:

const MY_EVENT = 'myEvent';

@Component({ tag: 'my-component' })
export class MyComponent {
  // ❌ Cannot use MY_EVENT variable - must use string literal
  @Listen('myEvent')  // Typo-prone and hard to refactor
  handleEvent() {}
  
  @Event({ eventName: 'myEvent' })  // Same issue here
  myEvent: EventEmitter;
}

GitHub Issue Number: N/A

What is the new behavior?

This PR introduces a new resolveVar() function that allows developers to use const variables and object properties in @Listen and @Event decorators. The function resolves variable values at compile-time, providing:

  • Type safety: Event names are resolved from const variables, enabling better IDE support
  • Refactoring support: Renaming a const variable automatically updates all decorator usages
  • Reduced typos: Using variables instead of strings reduces the chance of typos
  • Better organization: Event names can be centralized in const objects

The resolveVar() function is compile-time only and must be imported from @stencil/core:

import { Component, Listen, Event, EventEmitter, resolveVar } from '@stencil/core';

// Using const variables
const MY_EVENT = 'myEvent';

@Component({ tag: 'my-component' })
export class MyComponent {
  // ✅ Now supports variables
  @Listen(resolveVar(MY_EVENT))
  handleEvent() {}
  
  @Event({ eventName: resolveVar(MY_EVENT) })
  myEvent: EventEmitter;
}

// Using object properties
const EVENTS = {
  MY_EVENT: 'myEvent',
  OTHER_EVENT: 'otherEvent'
} as const;

@Component({ tag: 'my-component' })
export class MyComponent {
  // ✅ Supports object properties too
  @Listen(resolveVar(EVENTS.MY_EVENT))
  handleEvent() {}
  
  @Event({ eventName: resolveVar(EVENTS.OTHER_EVENT) })
  otherEvent: EventEmitter;
}

Features

  • ✅ Supports const variables with string literal values
  • ✅ Supports const variables with as const assertions
  • ✅ Supports object properties from const objects
  • ✅ Compile-time resolution - all resolveVar() calls are replaced with resolved string values
  • ✅ Comprehensive error handling with helpful diagnostic messages
  • ✅ Full TypeScript type checking support

Implementation Details

  • The resolveVar() function is detected during the decorator transformation phase
  • Variable values are resolved using the TypeScript type checker
  • All resolveVar() calls are replaced with their resolved string values at compile-time
  • No runtime implementation is needed - the function is purely a compile-time marker

Documentation

Documentation updates should be added to:

  • Stencil documentation site: Add resolveVar to the Events documentation
  • JSDoc comments have been added to the function declaration in stencil-public-runtime.ts

Does this introduce a breaking change?

  • Yes
  • No

This is a purely additive feature. Existing code using string literals in decorators continues to work exactly as before. The resolveVar() function is optional and can be adopted incrementally.

Testing

Unit Tests (Compiler Transformers)

  • src/compiler/transformers/test/decorator-utils.spec.ts
    • Tests for resolving const variables with string literals
    • Tests for resolving const variables with as const assertions
    • Tests for resolving object properties
    • Tests for error cases (non-resolvable variables, missing properties)

Integration Tests

  • src/compiler/transformers/test/parse-listeners.spec.ts

    • Tests @Listen(resolveVar(MY_EVENT)) with const variables
    • Tests @Listen(resolveVar(MY_EVENT)) with as const assertions
    • Tests @Listen(resolveVar(EVENTS.MY_EVENT)) with object properties
    • Verifies listeners metadata contains correct resolved event names
  • src/compiler/transformers/test/parse-events.spec.ts

    • Tests @Event({ eventName: resolveVar(MY_EVENT) }) with const variables
    • Tests @Event({ eventName: resolveVar(EVENTS.MY_EVENT) }) with object properties
    • Verifies events metadata contains correct resolved event names

Runtime Unit Tests

  • src/runtime/test/listen.spec.tsx

    • Tests @Listen(resolveVar(MY_EVENT)) - verifies listener works correctly
    • Tests @Listen(resolveVar(EVENTS.MY_EVENT)) - verifies listener works with object property
    • Tests event dispatching and receiving with resolved variable names
  • src/runtime/test/event.spec.tsx

    • Tests @Event({ eventName: resolveVar(MY_EVENT) }) - verifies event emits correctly
    • Tests @Event({ eventName: resolveVar(EVENTS.MY_EVENT) }) - verifies event emits with object property
    • Tests event listener receiving events with resolved variable names

End-to-End Tests

  • test/end-to-end/src/resolve-var-events/resolve-var-events.e2e.ts
    • Tests @Event({ eventName: resolveVar(MY_EVENT) }) - verifies event fires correctly
    • Tests @Listen(resolveVar(MY_EVENT)) - verifies listener receives events
    • Tests @Event({ eventName: resolveVar(EVENTS.MY_EVENT) }) with object property
    • Tests @Listen(resolveVar(EVENTS.MY_EVENT)) with object property
    • Tests integration scenarios with multiple events

All tests pass and follow existing testing patterns in the codebase.

Other information

Files Changed

Core Implementation:

  • src/declarations/stencil-public-runtime.ts - Added resolveVar function declaration
  • src/compiler/transformers/decorators-to-static/decorator-utils.ts - Added resolveVar detection and resolution logic
  • src/compiler/transformers/transform-utils.ts - Enhanced objectLiteralToObjectMap to handle resolveVar in object literals
  • src/compiler/transformers/decorators-to-static/listen-decorator.ts - Updated to pass diagnostics
  • src/compiler/transformers/decorators-to-static/event-decorator.ts - Updated to pass diagnostics
  • src/compiler/transformers/decorators-to-static/component-decorator.ts - Updated to pass diagnostics

Tests:

  • src/compiler/transformers/test/decorator-utils.spec.ts - Unit tests for resolveVar resolution
  • src/compiler/transformers/test/parse-listeners.spec.ts - Integration tests for @Listen with resolveVar
  • src/compiler/transformers/test/parse-events.spec.ts - Integration tests for @Event with resolveVar
  • src/runtime/test/listen.spec.tsx - Runtime tests for @Listen with resolveVar
  • src/runtime/test/event.spec.tsx - Runtime tests for @Event with resolveVar
  • test/end-to-end/src/resolve-var-events/resolve-var-events.tsx - E2E test component
  • test/end-to-end/src/resolve-var-events/resolve-var-events.e2e.ts - E2E tests

Error Handling

When resolveVar() cannot resolve a value at compile-time, it provides helpful error messages:

  • "resolveVar() cannot resolve the value of [variable name] at compile time. Only const variables and object properties with string literal values are supported."
  • "resolveVar() cannot find property [property name] on object [object name] at compile time."
  • "resolveVar() expects exactly one argument."

All errors are properly attached to the relevant AST nodes for better IDE integration.

Performance

No performance impact - resolveVar() is resolved at compile-time and completely removed from the output. The resolved string values are identical to what developers would have written manually.

Backward Compatibility

100% backward compatible. Existing code continues to work unchanged. The feature is opt-in and can be adopted incrementally.

@tamb tamb requested a review from a team as a code owner November 17, 2025 01:37
@tamb tamb changed the title feature/adds a compile time function that can be used in typescript c… feat/compile variables as arguments for decorators Nov 18, 2025
@tamb
Copy link
Contributor Author

tamb commented Nov 19, 2025

@johnjenkins
This is a different, and I think better, approach to solving the problem in another PR that is closed.

#6361

It's more in step with utility type functions already added.

@tamb tamb changed the title feat/compile variables as arguments for decorators feat(compiler) variables as arguments for decorators Nov 21, 2025
Copy link
Contributor

@johnjenkins johnjenkins left a comment

Choose a reason for hiding this comment

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

Lovely stuff!
I much prefer this approach (was a bit nervous regarding the potential cost in the last PR)

@johnjenkins johnjenkins added this pull request to the merge queue Nov 28, 2025
Merged via the queue into stenciljs:main with commit fa9a025 Nov 28, 2025
137 of 138 checks passed
@johnjenkins
Copy link
Contributor

johnjenkins commented Nov 28, 2025

@tamb - before I release this, can you please write some docs for it 🙏
(potentially under this section? https://stenciljs.com/docs/api#other (or wherever you think best))

As this is a new feature it will be a minor release (so don't make changes in the v4.38 versioned doc, instead make changes here)

@tamb
Copy link
Contributor Author

tamb commented Nov 29, 2025

@tamb - before I release this, can you please write some docs for it 🙏 (potentially under this section? https://stenciljs.com/docs/api#other (or wherever you think best))

As this is a new feature it will be a minor release (so don't make changes in the v4.38 versioned doc, instead make changes here)

Will this do @johnjenkins ?
stenciljs/site#1563

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.

2 participants