diff --git a/packages/@lwc/engine-core/src/framework/decorators/wire.ts b/packages/@lwc/engine-core/src/framework/decorators/wire.ts index 7abb57b1f3..f737421427 100644 --- a/packages/@lwc/engine-core/src/framework/decorators/wire.ts +++ b/packages/@lwc/engine-core/src/framework/decorators/wire.ts @@ -65,7 +65,15 @@ export default function wire< Class = LightningElement, >( // eslint-disable-next-line @typescript-eslint/no-unused-vars - adapter: WireAdapterConstructor, Value, Context>, + adapter: + | WireAdapterConstructor, Value, Context> + | { + adapter: WireAdapterConstructor< + ReplaceReactiveValues, + Value, + Context + >; + }, // eslint-disable-next-line @typescript-eslint/no-unused-vars config?: ReactiveConfig ): WireDecorator { diff --git a/packages/@lwc/integration-types/src/decorators/wire.ts b/packages/@lwc/integration-types/src/decorators/wire.ts index 6be62c1ab2..686d578540 100644 --- a/packages/@lwc/integration-types/src/decorators/wire.ts +++ b/packages/@lwc/integration-types/src/decorators/wire.ts @@ -15,6 +15,10 @@ type DeepConfig = { deep: { config: number } }; declare const testConfig: TestConfig; declare const testValue: TestValue; declare const TestAdapter: WireAdapterConstructor; +declare const TestAdapterWithImperative: { + (config: TestConfig): TestValue; + adapter: WireAdapterConstructor; +}; declare const AnyAdapter: any; declare const InvalidAdapter: object; declare const DeepConfigAdapter: WireAdapterConstructor; @@ -39,7 +43,7 @@ export class PropertyDecorators extends LightningElement { // Valid - basic @wire(TestAdapter, { config: 'config' }) basic?: TestValue; - @wire(TestAdapter, { config: '$config' }) + @wire(TestAdapter, { config: '$configProp' }) simpleReactive?: TestValue; @wire(TestAdapter, { config: '$nested.prop' }) nestedReactive?: TestValue; @@ -126,6 +130,72 @@ export class PropertyDecorators extends LightningElement { never?: never; } +/** Validations for decorated properties/fields */ +export class PropertyDecoratorsWithImperative extends LightningElement { + // Helper props + configProp = 'config' as const; + nested = { prop: 'config', invalid: 123 } as const; + // 'nested.prop' is not directly used, but helps validate that the reactive config resolution + // uses the object above, rather than a weird prop name + 'nested.prop' = false; + number = 123; + // --- VALID --- // + // Valid - basic + @wire(TestAdapterWithImperative, { config: 'config' }) + basic?: TestValue; + @wire(TestAdapterWithImperative, { config: '$configProp' }) + simpleReactive?: TestValue; + @wire(TestAdapterWithImperative, { config: '$nested.prop' }) + nestedReactive?: TestValue; + // Valid - as const + @wire(TestAdapterWithImperative, { config: 'config' } as const) + basicAsConst?: TestValue; + @wire(TestAdapterWithImperative, { config: '$configProp' } as const) + simpleReactiveAsConst?: TestValue; + // Valid - using `any` + @wire(TestAdapterWithImperative, {} as any) + configAsAny?: TestValue; + @wire(TestAdapterWithImperative, { config: 'config' }) + propAsAny?: any; + // Valid - prop assignment + @wire(TestAdapterWithImperative, { config: 'config' }) + nonNullAssertion!: TestValue; + @wire(TestAdapterWithImperative, { config: 'config' }) + explicitDefaultType: TestValue = testValue; + @wire(TestAdapterWithImperative, { config: 'config' }) + implicitDefaultType = testValue; + + // --- INVALID --- // + // @ts-expect-error Too many wire parameters + @wire(TestAdapterWithImperative, { config: 'config' }, {}) + tooManyWireParams?: TestValue; + // @ts-expect-error Bad config type + @wire(TestAdapterWithImperative, { bad: 'value' }) + badConfig?: TestValue; + // @ts-expect-error Bad prop type + @wire(TestAdapterWithImperative, { config: 'config' }) + badPropType?: { bad: 'value' }; + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const) + nonExistentReactiveProp?: TestValue; + + // --- AMBIGUOUS --- // + // Passing a config is optional because adapters don't strictly need to use it. + // Can we be smarter about the type and require a config, but only if the adapter does? + @wire(TestAdapterWithImperative) + noConfig?: TestValue; + // Because the basic type `string` could be _any_ string, we can't narrow it and compare against + // the component's props, so we must accept all string props, even if they're incorrect. + // We could technically be strict, and enforce that all configs objects use `as const`, but very + // few projects currently use it (there is no need) and the error reported is not simple to + // understand. + @wire(TestAdapterWithImperative, { config: 'incorrect' }) + wrongConfigButInferredAsString?: TestValue; + // People shouldn't do this, and they probably never (heh) will. TypeScript allows it, though. + @wire(TestAdapterWithImperative, { config: 'config' }) + never?: never; +} + /** Validations for decorated methods */ export class MethodDecorators extends LightningElement { // Helper props @@ -141,13 +211,13 @@ export class MethodDecorators extends LightningElement { basic(_: TestValue) {} @wire(TestAdapter, { config: 'config' }) async asyncMethod(_: TestValue) {} - @wire(TestAdapter, { config: '$config' }) + @wire(TestAdapter, { config: '$configProp' }) simpleReactive(_: TestValue) {} @wire(TestAdapter, { config: '$nested.prop' }) nestedReactive(_: TestValue) {} - @wire(TestAdapter, { config: '$config' }) + @wire(TestAdapter, { config: '$configProp' }) optionalParam(_?: TestValue) {} - @wire(TestAdapter, { config: '$config' }) + @wire(TestAdapter, { config: '$configProp' }) noParam() {} // Valid - as const @wire(TestAdapter, { config: 'config' } as const) @@ -222,6 +292,67 @@ export class MethodDecorators extends LightningElement { implicitDefaultType(_ = testValue) {} } +/** Validations for decorated methods */ +export class MethodDecoratorsWithImperative extends LightningElement { + // Helper props + configProp = 'config' as const; + nested = { prop: 'config', invalid: 123 } as const; + // 'nested.prop' is not directly used, but helps validate that the reactive config resolution + // uses the object above, rather than a weird prop name + 'nested.prop' = false; + number = 123; + // --- VALID --- // + // Valid - basic + @wire(TestAdapterWithImperative, { config: 'config' }) + basic(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: 'config' }) + async asyncMethod(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$configProp' }) + simpleReactive(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$nested.prop' }) + nestedReactive(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$configProp' }) + optionalParam(_?: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$configProp' }) + noParam() {} + // Valid - as const + @wire(TestAdapterWithImperative, { config: 'config' } as const) + basicAsConst(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$configProp' } as const) + simpleReactiveAsConst(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$nested.prop' } as const) + nestedReactiveAsConst(_: TestValue) {} + // Valid - using `any` + @wire(TestAdapterWithImperative, {} as any) + configAsAny(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: 'config' }) + paramAsAny(_: any) {} + + // --- INVALID --- // + // @ts-expect-error Too many wire parameters + @wire(TestAdapterWithImperative, { config: 'config' }, {}) + tooManyWireParams(_: TestValue) {} + // @ts-expect-error Too many method parameters + @wire(TestAdapterWithImperative, { config: 'config' }) + tooManyParameters(_a: TestValue, _b: TestValue) {} + + // --- AMBIGUOUS --- // + // Passing a config is optional because adapters don't strictly need to use it. + // Can we be smarter about the type and require a config, but only if the adapter does? + @wire(TestAdapterWithImperative) + noConfig(_: TestValue): void {} + // Because the basic type `string` could be _any_ string, we can't narrow it and compare against + // the component's props, so we must accept all string props, even if they're incorrect. + // We could technically be strict, and enforce that all configs objects use `as const`, but very + // few projects currently use it (there is no need) and the error reported is not simple to + // understand. + @wire(TestAdapterWithImperative, { config: 'incorrect' }) + wrongConfigButInferredAsString(_: TestValue): void {} + // Wire adapters shouldn't use default params, but the type system doesn't know the difference + @wire(TestAdapterWithImperative, { config: 'config' }) + implicitDefaultType(_ = testValue) {} +} + /** Validations for decorated getters */ export class GetterDecorators extends LightningElement { // Helper props @@ -244,7 +375,7 @@ export class GetterDecorators extends LightningElement { // we must return something. Since we don't have any data to return, we return `undefined` return undefined; } - @wire(TestAdapter, { config: '$config' }) + @wire(TestAdapter, { config: '$configProp' }) get simpleReactive() { return testValue; } @@ -341,6 +472,69 @@ export class GetterDecorators extends LightningElement { } } +/** Validations for decorated getters */ +export class GetterDecoratorsWithImperative extends LightningElement { + // Helper props + configProp = 'config' as const; + nested = { prop: 'config', invalid: 123 } as const; + // 'nested.prop' is not directly used, but helps validate that the reactive config resolution + // uses the object above, rather than a weird prop name + 'nested.prop' = false; + number = 123; + // --- VALID --- // + + // Valid - basic + @wire(TestAdapterWithImperative, { config: 'config' }) + get basic() { + return testValue; + } + @wire(TestAdapterWithImperative, { config: 'config' }) + get undefined() { + // The function implementation of a wired getter is ignored, but TypeScript enforces that + // we must return something. Since we don't have any data to return, we return `undefined` + return undefined; + } + @wire(TestAdapterWithImperative, { config: '$configProp' }) + get simpleReactive() { + return testValue; + } + @wire(TestAdapterWithImperative, { config: '$nested.prop' }) + get nestedReactive() { + return testValue; + } + // Valid - using `any` + @wire(TestAdapterWithImperative, {} as any) + get configAsAny() { + return testValue; + } + @wire(TestAdapterWithImperative, { config: 'config' }) + get valueAsAny() { + return null as any; + } + + // --- INVALID --- // + // @ts-expect-error Too many wire parameters + @wire(TestAdapterWithImperative, { config: 'config' }, {}) + get tooManyWireParams() { + return testValue; + } + // @ts-expect-error Bad config type + @wire(TestAdapterWithImperative, { bad: 'value' }) + get badConfig() { + return testValue; + } + // @ts-expect-error Bad value type + @wire(TestAdapterWithImperative, { config: 'config' }) + get badValueType() { + return { bad: 'value' }; + } + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const) + get nonExistentReactiveProp() { + return testValue; + } +} + /** Validations for decorated setters */ export class Setter extends LightningElement { // Helper props @@ -355,7 +549,7 @@ export class Setter extends LightningElement { // Valid - basic @wire(TestAdapter, { config: 'config' }) set basic(_: TestValue) {} - @wire(TestAdapter, { config: '$config' }) + @wire(TestAdapter, { config: '$configProp' }) set simpleReactive(_: TestValue) {} @wire(TestAdapter, { config: '$nested.prop' }) set nestedReactive(_: TestValue) {} @@ -411,3 +605,61 @@ export class Setter extends LightningElement { @wire(DeepConfigAdapter, { deep: { config: '$number' } } as const) set deepReactive(_: TestValue) {} } + +/** Validations for decorated setters */ +export class SetterWithImperative extends LightningElement { + // Helper props + configProp = 'config' as const; + nested = { prop: 'config', invalid: 123 } as const; + // 'nested.prop' is not directly used, but helps validate that the reactive config resolution + // uses the object above, rather than a weird prop name + 'nested.prop' = false; + number = 123; + // --- VALID --- // + + // Valid - basic + @wire(TestAdapterWithImperative, { config: 'config' }) + set basic(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$configProp' }) + set simpleReactive(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$nested.prop' }) + set nestedReactive(_: TestValue) {} + // Valid - as const + @wire(TestAdapterWithImperative, { config: 'config' } as const) + set basicAsConst(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$configProp' } as const) + set simpleReactiveAsConst(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: '$nested.prop' } as const) + set nestedReactiveAsConst(_: TestValue) {} + // Valid - using `any` + @wire(TestAdapterWithImperative, {} as any) + set configAsAny(_: TestValue) {} + @wire(TestAdapterWithImperative, { config: 'config' }) + set valueAsAny(_: any) {} + + // --- INVALID --- // + // @ts-expect-error Too many wire parameters + @wire(TestAdapterWithImperative, { config: 'config' }, {}) + set tooManyWireParams(_: TestValue) {} + // @ts-expect-error Bad config type + @wire(TestAdapterWithImperative, { bad: 'value' }) + set badConfig(_: TestValue) {} + // @ts-expect-error Bad value type + @wire(TestAdapterWithImperative, { config: 'config' }) + set badValueType(_: { bad: 'value' }) {} + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const) + set nonExistentReactiveProp(_: TestValue) {} + // @ts-expect-error Referenced reactive prop is the wrong type + @wire(TestAdapterWithImperative, { config: '$number' } as const) + set numberReactiveProp(_: TestValue) {} + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapterWithImperative, { config: '$nested.nonexistent' } as const) + set nonexistentNestedReactiveProp(_: TestValue) {} + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapterWithImperative, { config: '$nested.invalid' } as const) + set invalidNestedReactiveProp(_: TestValue) {} + // @ts-expect-error Incorrect non-reactive string literal type + @wire(TestAdapterWithImperative, { config: 'not reactive' } as const) + set nonReactiveStringLiteral(_: TestValue) {} +}