Skip to content

feat(types): add hidden helper to make inferring element types better @W-18442478#5362

Merged
wjhsf merged 4 commits intomasterfrom
wjh/api-helper-type
May 7, 2025
Merged

feat(types): add hidden helper to make inferring element types better @W-18442478#5362
wjhsf merged 4 commits intomasterfrom
wjh/api-helper-type

Conversation

@wjhsf
Copy link
Contributor

@wjhsf wjhsf commented May 5, 2025

Details

When rendering an LWC component, the component instance and element instance are two connected but distinct objects. The component instance inherits from LightningElement, while the element is an instance of HTMLElement with a few additional properties. Those properties are the @api-decorated properties from the component instance.

When the class in the following example gets used, the component instance would have two props, exposed and internal. The element instance only has one, exposed.

class Example extends LightningElement {
  @api exposed = 'hello'
  internal = 'secret'
}

Because component authors create components, but largely consume elements, we added a new helper type in LWC v7.0.0 to bridge the gap. The new LightningHTMLElement type accepts a component interface and returns the corresponding element interface. However, it has a critical limitation. There is no way, in TypeScript, to distinguish decorated properties from non-decorated properties. And so, by default, LightningHTMLElement returns an interface with all properties defined on the component, not just @api-decorated properties.

const example = createElement('c-example', { is: Example });
example.exposed satisfies string // ✅
example.internal satisfies string // ❌ not a type error, but it should be!

The workaround originally provided for this was to explicitly provide the correct interface.

createElement<{ exposed: string }>('c-example', { is: Example });
this.querySelector('c-example') as LightningHTMLElement<{ exposed: string }>

However, this approach is limited, as it must be done for every single usage of createElement/LightningHTMLElement. More importantly, it requires the component consumer to provide the interface, rather than the component author. Thus, this PR introduces an new hidden/"fake" property that components can use. If a component defines __lwc_public_property_types__, then the type from that property is used as the public interface. This new property is "fake", in that it only exists as a hint to the type system; it never has a value at runtime. And it's "hidden" because it's not part of the public LightningElement interface, it's just sniffed by the helper type.

class Example {
  @api exposed = 'hello'
  internal = 'secret'
  __lwc_public_property_types__?: { exposed: string }
}

const example = createElement('c-example', { is: Example });
example.exposed satisfies string // ✅
example.internal // ✅ type error, as expected!

Closes #4292.

Does this pull request introduce a breaking change?

  • 😮‍💨 No, it does not introduce a breaking change.
  • 💔 Yes, it does introduce a breaking change.

Does this pull request introduce an observable change?

  • 🤞 No, it does not introduce an observable change.
  • 🔬 Yes, it does include an observable change.

GUS work item

@wjhsf wjhsf requested a review from a team as a code owner May 5, 2025 19:31
@jhefferman-sfdc
Copy link
Contributor

Such a beautiful description, thank you!

@wjhsf wjhsf enabled auto-merge (squash) May 7, 2025 18:24
@wjhsf wjhsf merged commit be20445 into master May 7, 2025
6 checks passed
@wjhsf wjhsf deleted the wjh/api-helper-type branch May 7, 2025 18:27
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.

Type definition for createElement is inaccurate

2 participants