lwc
lwc copied to clipboard
feat(engine): support imperative wire adapters & fix wire adapter config handling
Details
Improve the type definition of wire decorator to support Imperative functions that also carry wire adapters.
This change also improves the type checking for the config param. The types are more closely validated, error messages are scoped better and are more concise.
Does this pull request introduce a breaking change?
- 💔 Yes, it does introduce a breaking change.
This is a breaking change for TypeScript users only. This expands what is allowed to be passed to the decorator as an adapter and tightens config types.
Does this pull request introduce an observable change?
- 🔬 Yes, it does include an observable change.
wire decorator type checking is different. No runtime behavior changes.
/nucleus test
Breaking change, so marking as v9 and "no merge."
Thanks a bunch for this!
Fixes #5082
Awesome! Look forward to it landing.
🤔 One thing that I don't like about the current implementation is that it requires modern decorators, but most SFDX projects are still configured to use legacy/experimental decorators. I wonder if this approach means we could restore support for experimental decorators.
@aheber do you think you could split the .adapter change into a separate PR, using the current type def? Then we could add that as a feature (should be able to land in v8) and focus on the refactor separately (would land in v9).
// `Class` appears unused, but by doing `Name extends keyof Class`, the result type preserves
// the optional/required status of each prop. If we just do `Name extends PropertyKey`, that
// necessary information is lost.
type ExpectedTarget<Value, Class, Name extends keyof Class> = {
[K in Name]: Value;
};
interface WireDecorator<Value, Class, Name extends keyof Class> {
(
target: unknown,
context: // A wired prop doesn't have any data on creation, so we must allow `undefined`
| ClassFieldDecoratorContext<Class, Value | undefined>
| ClassMethodDecoratorContext<
Class,
// When a wire adapter is typed as `WireAdapterConstructor`, then this `Value`
// generic is inferred as the value used by the adapter for all decorator contexts
// (field/method/getter/setter). But when the adapter is typed as `any`, then
// decorated methods have `Value` inferred as the full method. (I'm not sure why.)
// This conditional checks `Value` so that we get the correct decorator context.
Value extends (value: any) => any ? Value : (this: Class, value: Value) => void
>
// The implementation of a wired getter/setter is ignored; they are treated identically
// to wired props. Wired props don't have data on creation, so we must allow `undefined`
| ClassGetterDecoratorContext<Class, Value | undefined>
| ClassSetterDecoratorContext<Class, Value>
): void;
(target: ExpectedTarget<Value, Class, Name>, prop: Name): void;
(
target: Class,
prop: Name,
descriptor: // getter/setter
| TypedPropertyDescriptor<Value>
// method with param
| TypedPropertyDescriptor<(value: Value) => any>
// method with no param
| TypedPropertyDescriptor<() => any>
): void;
}
Here's an updated type for WireDecorator (along with a new generic param Name added to the wire function). It works for our current test cases when using experimental decorators, but the last overload makes it explode when using modern decorators. I think it's probably something to do with the way the signature changes the inferred generic params, but I don't have time to look into it now.
I wonder if this approach means we could restore support for experimental decorators.
Turns out we can't, because of https://github.com/salesforce/lwc/issues/5087#issuecomment-2583214496. The LWC compiler required decorators to be preserved, and tsc only preserves modern decorators, not experimental.
I've isolated the change for Imperative support into this PR: #5132
We can continue to refine this without holding up that compatibility fix.