diff --git a/packages/@lwc/engine-core/src/framework/decorators/api.ts b/packages/@lwc/engine-core/src/framework/decorators/api.ts index 62c7e1d2e7..663df2ae00 100644 --- a/packages/@lwc/engine-core/src/framework/decorators/api.ts +++ b/packages/@lwc/engine-core/src/framework/decorators/api.ts @@ -8,9 +8,9 @@ import { assert, isFunction, isNull, toString } from '@lwc/shared'; import { logError } from '../../shared/logger'; import { isInvokingRender, isBeingConstructed } from '../invoker'; import { componentValueObserved, componentValueMutated } from '../mutation-tracker'; -import { LightningElement } from '../base-lightning-element'; import { getAssociatedVM } from '../vm'; import { isUpdatingTemplate, getVMBeingRendered } from '../template'; +import type { LightningElement } from '../base-lightning-element'; /** * The `@api` decorator marks public fields and public methods in @@ -21,7 +21,7 @@ export default function api( // eslint-disable-next-line @typescript-eslint/no-unused-vars value: unknown, // eslint-disable-next-line @typescript-eslint/no-unused-vars - context: ClassMemberDecoratorContext | string | symbol + context: ClassMemberDecoratorContext ): void { if (process.env.NODE_ENV !== 'production') { assert.fail(`@api decorator can only be used as a decorator function.`); diff --git a/packages/@lwc/engine-core/src/framework/decorators/track.ts b/packages/@lwc/engine-core/src/framework/decorators/track.ts index b2ca91bca9..a922aed165 100644 --- a/packages/@lwc/engine-core/src/framework/decorators/track.ts +++ b/packages/@lwc/engine-core/src/framework/decorators/track.ts @@ -9,21 +9,23 @@ import { componentValueObserved } from '../mutation-tracker'; import { isInvokingRender } from '../invoker'; import { getAssociatedVM } from '../vm'; import { getReactiveProxy } from '../membrane'; -import { LightningElement } from '../base-lightning-element'; import { isUpdatingTemplate, getVMBeingRendered } from '../template'; import { updateComponentValue } from '../update-component-value'; import { logError } from '../../shared/logger'; +import type { LightningElement } from '../base-lightning-element'; /** * The `@track` decorator function marks field values as reactive in * LWC Components. This function can also be invoked directly * with any value to obtain the trackable version of the value. */ +export default function track(target: undefined, context: ClassFieldDecoratorContext): void; +export default function track(target: T, context?: never): T; export default function track( - value: unknown, - context: ClassMemberDecoratorContext | string | symbol -): void; -export default function track(target: T): T { + target: unknown, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context?: ClassFieldDecoratorContext +): unknown { if (arguments.length === 1) { return getReactiveProxy(target); } diff --git a/packages/@lwc/engine-core/src/framework/decorators/wire.ts b/packages/@lwc/engine-core/src/framework/decorators/wire.ts index 876abe0b22..7abb57b1f3 100644 --- a/packages/@lwc/engine-core/src/framework/decorators/wire.ts +++ b/packages/@lwc/engine-core/src/framework/decorators/wire.ts @@ -5,11 +5,47 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { assert } from '@lwc/shared'; -import { LightningElement } from '../base-lightning-element'; import { componentValueObserved } from '../mutation-tracker'; import { getAssociatedVM } from '../vm'; -import { WireAdapterConstructor } from '../wiring'; import { updateComponentValue } from '../update-component-value'; +import type { LightningElement } from '../base-lightning-element'; +import type { + ConfigValue, + ContextValue, + ReplaceReactiveValues, + WireAdapterConstructor, +} from '../wiring'; + +/** + * The decorator returned by `@wire()`; not the `wire` function. + * + * For TypeScript users: + * - If you are seeing an unclear error message, ensure that both the type of the decorated prop and + * the config used match the types expected by the wire adapter. + * - String literal types in the config are resolved to the corresponding prop on the component. + * For example, a component with `id = 555` and `@wire(getBook, {id: "$id"} as const) book` will + * have `"$id"` resolve to type `number`. + */ +interface WireDecorator { + ( + target: unknown, + context: // A wired prop doesn't have any data on creation, so we must allow `undefined` + | ClassFieldDecoratorContext + | 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 + | ClassSetterDecoratorContext + ): void; +} /** * Decorator factory to wire a property or method to a wire adapter data source. @@ -22,12 +58,17 @@ import { updateComponentValue } from '../update-component-value'; * \@wire(getBook, { id: '$bookId'}) book; * } */ -export default function wire( +export default function wire< + ReactiveConfig extends ConfigValue = ConfigValue, + Value = any, + Context extends ContextValue = ContextValue, + Class = LightningElement, +>( // eslint-disable-next-line @typescript-eslint/no-unused-vars - adapter: WireAdapterConstructor, + adapter: WireAdapterConstructor, Value, Context>, // eslint-disable-next-line @typescript-eslint/no-unused-vars - config?: Record -): (value: unknown, context: ClassMemberDecoratorContext | string | symbol) => void { + config?: ReactiveConfig +): WireDecorator { if (process.env.NODE_ENV !== 'production') { assert.fail('@wire(adapter, config?) may only be used as a decorator.'); } diff --git a/packages/@lwc/engine-core/src/framework/wiring/index.ts b/packages/@lwc/engine-core/src/framework/wiring/index.ts index 0732966f8d..a152307193 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/index.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/index.ts @@ -14,6 +14,7 @@ export { ContextProvider, ContextValue, DataCallback, + ReplaceReactiveValues, WireAdapter, WireAdapterConstructor, WireAdapterSchemaValue, diff --git a/packages/@lwc/engine-core/src/framework/wiring/types.ts b/packages/@lwc/engine-core/src/framework/wiring/types.ts index 5216ae5670..8eba2060d9 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/types.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/types.ts @@ -87,3 +87,39 @@ export type RegisterContextProviderFn = ( adapterContextToken: string, onContextSubscription: WireContextSubscriptionCallback ) => void; + +/** Resolves a property chain to the corresponding value on the target type. */ +type ResolveReactiveValue< + /** The object to search for properties; initially the component. */ + Target, + /** A string representing a chain of of property keys, e.g. "data.user.name". */ + Keys extends string, +> = Keys extends `${infer FirstKey}.${infer Rest}` + ? // If the string is "a.b.c", check if "a" is a prop on the target object + FirstKey extends keyof Target + ? // If "a" exists on the target, check `target["a"]` for "b.c" + ResolveReactiveValue + : undefined + : // The string has no ".", use the full string as the key (e.g. we've reached "c" in "a.b.c") + Keys extends keyof Target + ? Target[Keys] + : undefined; + +/** + * Detects if the `Value` type is a property chain starting with "$". If so, it resolves the + * properties to the corresponding value on the target type. + */ +type ResolveValueIfReactive = Value extends string + ? string extends Value // `Value` is type `string` + ? // Workaround for not being able to enforce `as const` assertions -- we don't know if this + // is a true string value (e.g. `@wire(adapter, {val: 'str'})`) or if it's a reactive prop + // (e.g. `@wire(adapter, {val: '$number'})`), so we have to go broad to avoid type errors. + any + : Value extends `$${infer Keys}` // String literal starting with "$", e.g. `$prop` + ? ResolveReactiveValue + : Value // String literal *not* starting with "$", e.g. `"hello world"` + : Value; // non-string type + +export type ReplaceReactiveValues = { + [K in keyof Config]: ResolveValueIfReactive; +}; diff --git a/packages/@lwc/integration-types/package.json b/packages/@lwc/integration-types/package.json index 283fa82c7a..ca85a9ba7a 100644 --- a/packages/@lwc/integration-types/package.json +++ b/packages/@lwc/integration-types/package.json @@ -5,8 +5,7 @@ "description": "Type validation for LWC packages", "type": "module", "scripts": { - "test": "node ./scripts/update-paths.js --check && tsc && yarn run test:experimental-decorators", - "test:experimental-decorators": "tsc -p tsconfig.experimental-decorators.json", + "test": "node ./scripts/update-paths.js --check && tsc", "playground": "rollup -c src/playground/rollup.config.js --watch" }, "dependencies": { diff --git a/packages/@lwc/integration-types/src/decorators.ts b/packages/@lwc/integration-types/src/decorators.ts deleted file mode 100644 index 91ab95f016..0000000000 --- a/packages/@lwc/integration-types/src/decorators.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2024, Salesforce, Inc. - * All rights reserved. - * SPDX-License-Identifier: MIT - * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT - */ - -/** - * This file contains basic decorator usage and is tested with `experimentalDecorators` set to both - * `true` and `false`, to validate that the method signatures work in both cases. - */ - -import { LightningElement, api, track, wire } from 'lwc'; - -class FakeWireAdapter { - update() {} - connect() {} - disconnect() {} -} - -export default class extends LightningElement { - @api apiProp?: string; - @wire(FakeWireAdapter, {}) wireProp?: number; - @track trackedProp?: Record; -} diff --git a/packages/@lwc/integration-types/src/decorators/api.ts b/packages/@lwc/integration-types/src/decorators/api.ts new file mode 100644 index 0000000000..1fbaae489e --- /dev/null +++ b/packages/@lwc/integration-types/src/decorators/api.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import { LightningElement, api } from 'lwc'; + +// @ts-expect-error bare decorator cannot be used +api(); + +// @ts-expect-error decorator doesn't work on classes +@api +export default class Test extends LightningElement { + @api optionalProperty?: string; + @api propertyWithDefault = true; + @api nonNullAssertedProperty!: object; + @api method() {} + @api getter(): undefined {} + @api setter(_: string) {} +} diff --git a/packages/@lwc/integration-types/src/decorators/track.ts b/packages/@lwc/integration-types/src/decorators/track.ts new file mode 100644 index 0000000000..b1237aaa89 --- /dev/null +++ b/packages/@lwc/integration-types/src/decorators/track.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +import { LightningElement, track } from 'lwc'; + +// This is okay! track has a non-decorator signature +track(123); +// This is okay because we treat implicit and explicit `undefined` identically +track(123, undefined); +// @ts-expect-error wrong number of arguments +track(); +// @ts-expect-error wrong number of arguments +track({}, {}); + +// @ts-expect-error doesn't work on classes +@track +export default class Test extends LightningElement { + @track optionalProperty?: string; + @track propertyWithDefault = true; + @track nonNullAssertedProperty!: object; + // @ts-expect-error cannot be used on methods + @track method() {} + // @ts-expect-error cannot be used on getters + @track getter(): undefined {} + // @ts-expect-error cannot be used on setters + @track setter(_: string) {} +} diff --git a/packages/@lwc/integration-types/src/decorators/wire.ts b/packages/@lwc/integration-types/src/decorators/wire.ts new file mode 100644 index 0000000000..6a850a0d46 --- /dev/null +++ b/packages/@lwc/integration-types/src/decorators/wire.ts @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import { LightningElement, WireAdapterConstructor, wire } from 'lwc'; + +type TestConfig = { config: 'config' }; +type TestValue = { value: 'value' }; +type TestContext = { context: 'context' }; +type DeepConfig = { deep: { config: number } }; + +declare const testConfig: TestConfig; +declare const testValue: TestValue; +declare const TestAdapter: WireAdapterConstructor; +declare const AnyAdapter: any; +declare const InvalidAdapter: object; +declare const DeepConfigAdapter: WireAdapterConstructor; + +// @ts-expect-error bare decorator cannot be used +wire(FakeWireAdapter, { config: 'config' })(); + +// @ts-expect-error decorator cannot be used on classes +@wire(FakeWireAdapter, { config: 'config' }) +export class InvalidContext extends LightningElement {} + +/** Validations for decorated properties/fields */ +export class PropertyDecorators 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(TestAdapter, { config: 'config' }) + basic?: TestValue; + @wire(TestAdapter, { config: '$config' }) + simpleReactive?: TestValue; + @wire(TestAdapter, { config: '$nested.prop' }) + nestedReactive?: TestValue; + @wire(TestAdapter, testConfig) + configVariable?: TestValue; + // Valid - as const + @wire(TestAdapter, { config: 'config' } as const) + basicAsConst?: TestValue; + @wire(TestAdapter, { config: '$configProp' } as const) + simpleReactiveAsConst?: TestValue; + @wire(TestAdapter, { config: '$nested.prop' } as const) + nestedReactiveAsConst?: TestValue; + // Valid - using `any` + @wire(TestAdapter, {} as any) + configAsAny?: TestValue; + @wire(TestAdapter, { config: 'config' }) + propAsAny?: any; + @wire(AnyAdapter, { config: 'config' }) + adapterAsAny?: TestValue; + @wire(AnyAdapter, { other: ['value'] }) + adapterAsAnyOtherValues?: null; + // Valid - prop assignment + @wire(TestAdapter, { config: 'config' }) + nonNullAssertion!: TestValue; + @wire(TestAdapter, { config: 'config' }) + explicitDefaultType: TestValue = testValue; + @wire(TestAdapter, { config: 'config' }) + implicitDefaultType = testValue; + + // --- INVALID --- // + // @ts-expect-error Invalid adapter type + @wire(InvalidAdapter, { config: 'config' }) + invalidAdapter?: TestValue; + // @ts-expect-error Missing wire parameters + @wire() + missingWireParams?: TestValue; + // @ts-expect-error Too many wire parameters + @wire(TestAdapter, { config: 'config' }, {}) + tooManyWireParams?: TestValue; + // @ts-expect-error Bad config type + @wire(TestAdapter, { bad: 'value' }) + badConfig?: TestValue; + // @ts-expect-error Bad prop type + @wire(TestAdapter, { config: 'config' }) + badPropType?: { bad: 'value' }; + // @ts-expect-error Prop must be optional or assigned in constructor + @wire(TestAdapter, { config: 'config' }) notOptional: TestValue; + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapter, { config: '$nonexistentProp' } as const) + nonExistentReactiveProp?: TestValue; + // @ts-expect-error Referenced reactive prop is the wrong type + @wire(TestAdapter, { config: '$number' } as const) + numberReactiveProp?: TestValue; + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.nonexistent' } as const) + nonexistentNestedReactiveProp?: TestValue; + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.invalid' } as const) + invalidNestedReactiveProp?: TestValue; + // @ts-expect-error Incorrect non-reactive string literal type + @wire(TestAdapter, { config: 'not reactive' } as const) + nonReactiveStringLiteral?: TestValue; + // @ts-expect-error Nested props are not reactive - only top level + @wire(DeepConfigAdapter, { deep: { config: '$number' } } as const) + deepReactive?: TestValue; + // @ts-expect-error Looks like a method, but it's actually a prop + @wire(TestAdapter, { config: 'config' }) + propValueIsMethod = function (this: PropertyDecorators, _: TestValue): void {}; + + // --- 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(TestAdapter) + 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(TestAdapter, { config: 'incorrect' }) + wrongConfigButInferredAsString?: TestValue; + // People shouldn't do this, and they probably never (heh) will. TypeScript allows it, though. + @wire(TestAdapter, { config: 'config' }) + never?: never; +} + +/** Validations for decorated methods */ +export class MethodDecorators 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(TestAdapter, { config: 'config' }) + basic(_: TestValue) {} + @wire(TestAdapter, { config: 'config' }) + async asyncMethod(_: TestValue) {} + @wire(TestAdapter, { config: '$config' }) + simpleReactive(_: TestValue) {} + @wire(TestAdapter, { config: '$nested.prop' }) + nestedReactive(_: TestValue) {} + @wire(TestAdapter, { config: '$config' }) + optionalParam(_?: TestValue) {} + @wire(TestAdapter, { config: '$config' }) + noParam() {} + // Valid - as const + @wire(TestAdapter, { config: 'config' } as const) + basicAsConst(_: TestValue) {} + @wire(TestAdapter, { config: '$configProp' } as const) + simpleReactiveAsConst(_: TestValue) {} + @wire(TestAdapter, { config: '$nested.prop' } as const) + nestedReactiveAsConst(_: TestValue) {} + // Valid - using `any` + @wire(TestAdapter, {} as any) + configAsAny(_: TestValue) {} + @wire(TestAdapter, { config: 'config' }) + paramAsAny(_: any) {} + @wire(AnyAdapter, { config: 'config' }) + adapterAsAny(_: TestValue) {} + + // --- INVALID --- // + // @ts-expect-error Invalid adapter type + @wire(InvalidAdapter, { config: 'config' }) + invalidAdapter(_: TestValue) {} + // @ts-expect-error Missing wire parameters + @wire() + missingWireParams() {} + // @ts-expect-error Too many wire parameters + @wire(TestAdapter, { config: 'config' }, {}) + tooManyWireParams(_: TestValue) {} + // @ts-expect-error Too many method parameters + @wire(TestAdapter, { config: 'config' }) + tooManyParameters(_a: TestValue, _b: TestValue) {} + // @ts-expect-error Bad config type + @wire(TestAdapter, { bad: 'value' }) + badConfig(_: TestValue): void {} + // @ts-expect-error Bad prop type + @wire(TestAdapter, { config: 'config' }) + badParamType(_: { bad: 'value' }): void {} + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapter, { config: '$nonexistentProp' } as const) + nonExistentReactiveProp(_: TestValue): void {} + // @ts-expect-error Referenced reactive prop is the wrong type + @wire(TestAdapter, { config: '$number' } as const) + numberReactiveProp(_: TestValue): void {} + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.nonexistent' } as const) + nonexistentNestedReactiveProp(_: TestValue): void {} + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.invalid' } as const) + invalidNestedReactiveProp(_: TestValue): void {} + // @ts-expect-error Incorrect non-reactive string literal type + @wire(TestAdapter, { config: 'not reactive' } as const) + nonReactiveStringLiteral(_: TestValue): void {} + // @ts-expect-error Nested props are not reactive - only top level + @wire(DeepConfigAdapter, { deep: { config: '$number' } } as const) + deepReactive(_: TestValue): void {} + // @ts-expect-error Param type looks like decorated method (validating type inference workaround) + @wire(TestAdapter, { config: 'config' }) + paramIsMethod(_: (inner: TestValue) => void) {} + + // --- 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(TestAdapter) + 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(TestAdapter, { config: 'incorrect' }) + wrongConfigButInferredAsString(_: TestValue): void {} + // Wire adapters shouldn't use default params, but the type system doesn't know the difference + @wire(TestAdapter, { config: 'config' }) + implicitDefaultType(_ = testValue) {} +} + +/** Validations for decorated getters */ +export class GetterDecorators 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(TestAdapter, { config: 'config' }) + get basic() { + return testValue; + } + @wire(TestAdapter, { 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(TestAdapter, { config: '$config' }) + get simpleReactive() { + return testValue; + } + @wire(TestAdapter, { config: '$nested.prop' }) + get nestedReactive() { + return testValue; + } + // Valid - as const + @wire(TestAdapter, { config: 'config' } as const) + get basicAsConst() { + return testValue; + } + @wire(TestAdapter, { config: '$configProp' } as const) + get simpleReactiveAsConst() { + return testValue; + } + @wire(TestAdapter, { config: '$nested.prop' } as const) + get nestedReactiveAsConst() { + return testValue; + } + // Valid - using `any` + @wire(TestAdapter, {} as any) + get configAsAny() { + return testValue; + } + @wire(TestAdapter, { config: 'config' }) + get valueAsAny() { + return null as any; + } + @wire(AnyAdapter, { config: 'config' }) + get adapterAsAny() { + return testValue; + } + @wire(AnyAdapter, { config: 'config' }) + get anyAdapterOtherValue() { + return 12345; + } + + // --- INVALID --- // + // @ts-expect-error Invalid adapter type + @wire(InvalidAdapter, { config: 'config' }) + get invalidAdapter() { + return testValue; + } + // @ts-expect-error Too many wire parameters + @wire(TestAdapter, { config: 'config' }, {}) + get tooManyWireParams() { + return testValue; + } + // @ts-expect-error Missing wire parameters + @wire() + get missingWireParams() { + return testValue; + } + // @ts-expect-error Bad config type + @wire(TestAdapter, { bad: 'value' }) + get badConfig() { + return testValue; + } + // @ts-expect-error Bad value type + @wire(TestAdapter, { config: 'config' }) + get badValueType() { + return { bad: 'value' }; + } + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapter, { config: '$nonexistentProp' } as const) + get nonExistentReactiveProp() { + return testValue; + } + // @ts-expect-error Referenced reactive prop is the wrong type + @wire(TestAdapter, { config: '$number' } as const) + get numberReactiveProp() { + return testValue; + } + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.nonexistent' } as const) + get nonexistentNestedReactiveProp() { + return testValue; + } + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.invalid' } as const) + get invalidNestedReactiveProp() { + return testValue; + } + // @ts-expect-error Incorrect non-reactive string literal type + @wire(TestAdapter, { config: 'not reactive' } as const) + get nonReactiveStringLiteral() { + return testValue; + } + // @ts-expect-error Nested props are not reactive - only top level + @wire(DeepConfigAdapter, { deep: { config: '$number' } } as const) + get deepReactive() { + return testValue; + } +} + +/** Validations for decorated setters */ +export class Setter 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(TestAdapter, { config: 'config' }) + set basic(_: TestValue) {} + @wire(TestAdapter, { config: '$config' }) + set simpleReactive(_: TestValue) {} + @wire(TestAdapter, { config: '$nested.prop' }) + set nestedReactive(_: TestValue) {} + // Valid - as const + @wire(TestAdapter, { config: 'config' } as const) + set basicAsConst(_: TestValue) {} + @wire(TestAdapter, { config: '$configProp' } as const) + set simpleReactiveAsConst(_: TestValue) {} + @wire(TestAdapter, { config: '$nested.prop' } as const) + set nestedReactiveAsConst(_: TestValue) {} + // Valid - using `any` + @wire(TestAdapter, {} as any) + set configAsAny(_: TestValue) {} + @wire(TestAdapter, { config: 'config' }) + set valueAsAny(_: any) {} + @wire(AnyAdapter, { config: 'config' }) + set adapterAsAny(_: TestValue) {} + @wire(AnyAdapter, { config: 'config' }) + set anyAdapterOtherValue(_: 12345) {} + + // --- INVALID --- // + // @ts-expect-error Invalid adapter type + @wire(InvalidAdapter, { config: 'config' }) + set invalidAdapter(_: TestValue) {} + // @ts-expect-error Too many wire parameters + @wire(TestAdapter, { config: 'config' }, {}) + set tooManyWireParams(_: TestValue) {} + // @ts-expect-error Missing wire parameters + @wire() + set missingWireParams(_: TestValue) {} + // @ts-expect-error Bad config type + @wire(TestAdapter, { bad: 'value' }) + set badConfig(_: TestValue) {} + // @ts-expect-error Bad value type + @wire(TestAdapter, { config: 'config' }) + set badValueType(_: { bad: 'value' }) {} + // @ts-expect-error Referenced reactive prop does not exist + @wire(TestAdapter, { config: '$nonexistentProp' } as const) + set nonExistentReactiveProp(_: TestValue) {} + // @ts-expect-error Referenced reactive prop is the wrong type + @wire(TestAdapter, { config: '$number' } as const) + set numberReactiveProp(_: TestValue) {} + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.nonexistent' } as const) + set nonexistentNestedReactiveProp(_: TestValue) {} + // @ts-expect-error Referenced nested reactive prop does not exist + @wire(TestAdapter, { config: '$nested.invalid' } as const) + set invalidNestedReactiveProp(_: TestValue) {} + // @ts-expect-error Incorrect non-reactive string literal type + @wire(TestAdapter, { config: 'not reactive' } as const) + set nonReactiveStringLiteral(_: TestValue) {} + // @ts-expect-error Nested props are not reactive - only top level + @wire(DeepConfigAdapter, { deep: { config: '$number' } } as const) + set deepReactive(_: TestValue) {} +} diff --git a/packages/@lwc/integration-types/tsconfig.experimental-decorators.json b/packages/@lwc/integration-types/tsconfig.experimental-decorators.json deleted file mode 100644 index 31e8f6fcb3..0000000000 --- a/packages/@lwc/integration-types/tsconfig.experimental-decorators.json +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used for tests that need to have the `experimentalDecorators` flag set. -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "experimentalDecorators": true - }, - "files": ["./src/decorators.ts"] -} diff --git a/playground/jsconfig.json b/playground/jsconfig.json deleted file mode 100644 index cd4d410233..0000000000 --- a/playground/jsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "compilerOptions": { - "experimentalDecorators": true - } -}