From 488bfff40f72c461b27b7d2a4569fa3f2b9be702 Mon Sep 17 00:00:00 2001 From: Heber Date: Fri, 27 Dec 2024 10:37:39 -0700 Subject: [PATCH 1/3] feat(engine): support imperative wire adapters & fix wire adapter config handling --- .../src/framework/decorators/wire.ts | 10 +- .../engine-core/src/framework/wiring/index.ts | 2 +- .../engine-core/src/framework/wiring/types.ts | 61 ++-- .../integration-types/src/decorators/wire.ts | 330 +++++++++++++++++- 4 files changed, 348 insertions(+), 55 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/decorators/wire.ts b/packages/@lwc/engine-core/src/framework/decorators/wire.ts index 7abb57b1f3..d20b57b239 100644 --- a/packages/@lwc/engine-core/src/framework/decorators/wire.ts +++ b/packages/@lwc/engine-core/src/framework/decorators/wire.ts @@ -11,8 +11,8 @@ import { updateComponentValue } from '../update-component-value'; import type { LightningElement } from '../base-lightning-element'; import type { ConfigValue, + ConfigWithReactiveValues, ContextValue, - ReplaceReactiveValues, WireAdapterConstructor, } from '../wiring'; @@ -59,15 +59,17 @@ interface WireDecorator { * } */ export default function wire< - ReactiveConfig extends ConfigValue = ConfigValue, + ExpectedConfig extends ConfigValue = ConfigValue, Value = any, Context extends ContextValue = ContextValue, Class = LightningElement, >( // eslint-disable-next-line @typescript-eslint/no-unused-vars - adapter: WireAdapterConstructor, Value, Context>, + adapter: + | WireAdapterConstructor + | { adapter: WireAdapterConstructor }, // eslint-disable-next-line @typescript-eslint/no-unused-vars - config?: ReactiveConfig + config?: ConfigWithReactiveValues ): 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 1375202ccb..e48faae315 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/index.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/index.ts @@ -10,12 +10,12 @@ export { createContextProviderWithRegister, createContextWatcher } from './conte export { ConfigCallback, ConfigValue, + ConfigWithReactiveValues, ContextConsumer, ContextProvider, ContextProviderOptions, 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 8eba2060d9..30d9df2f7b 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/types.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/types.ts @@ -88,38 +88,35 @@ export type RegisterContextProviderFn = ( 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. + * build a union of reactive property strings that would be compatible with the expected type + * + * exclude properties that are present on LightningElement, should help performance and avoid circular references that are inherited + * + * when looking for compatible types mark all optional properties as required so we can correctly evaluate their resolved types */ -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; +type ReactivePropertyOfCompoent = PrefixDollarSign< + PropertiesOfType>, ExpectedType> +>; + +/** utility type */ +type PrefixDollarSign = `${'$'}${T}`; + +/** recursively find all properties on the target that are of a compatible type, returning their paths as strings */ +type PropertiesOfType = { + [K in keyof Target]: Target[K] extends ExpectedType + ? `${Extract}` + : Target[K] extends object // If the value is an object, recursively check its properties + ? PropertiesOfType extends infer R // Check if any property in the object matches `U` + ? R extends never + ? never // If no compatible keys are found, exclude the key + : `${Extract}.${Extract}` // If compatible nested keys are found, include the key + : never + : never; +}[keyof Target]; + +/** wire decorator's config can be the literal type defined or a property from component that is compatible with the expected type */ +export type ConfigWithReactiveValues = { + // allow the original config value and also any valid reactive strings + [K in keyof Config]: Config[K] | ReactivePropertyOfCompoent; }; diff --git a/packages/@lwc/integration-types/src/decorators/wire.ts b/packages/@lwc/integration-types/src/decorators/wire.ts index 6be62c1ab2..e5652d318b 100644 --- a/packages/@lwc/integration-types/src/decorators/wire.ts +++ b/packages/@lwc/integration-types/src/decorators/wire.ts @@ -15,6 +15,11 @@ type DeepConfig = { deep: { config: number } }; declare const testConfig: TestConfig; declare const testValue: TestValue; declare const TestAdapter: WireAdapterConstructor; +declare const TestAdapterNoConfig: WireAdapterConstructor; +declare const TestAdapterWithImperative: { + (config: TestConfig): TestValue; + adapter: WireAdapterConstructor; +}; declare const AnyAdapter: any; declare const InvalidAdapter: object; declare const DeepConfigAdapter: WireAdapterConstructor; @@ -35,11 +40,13 @@ export class PropertyDecorators extends LightningElement { // uses the object above, rather than a weird prop name 'nested.prop' = false; number = 123; + anyProp: any; + // --- VALID --- // // 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; @@ -68,6 +75,12 @@ export class PropertyDecorators extends LightningElement { explicitDefaultType: TestValue = testValue; @wire(TestAdapter, { config: 'config' }) implicitDefaultType = testValue; + @wire(TestAdapter, { config: '$anyProp' }) + refAnyTypeProp = testValue; + @wire(TestAdapter, { config: '$anyProp.nested.because.any' }) + refAnyTypeNestedProp = testValue; + @wire(TestAdapterNoConfig) + adapterRefusesConfig?: TestValue; // --- INVALID --- // // @ts-expect-error Invalid adapter type @@ -102,30 +115,91 @@ export class PropertyDecorators extends LightningElement { // @ts-expect-error Incorrect non-reactive string literal type @wire(TestAdapter, { config: 'not reactive' } as const) nonReactiveStringLiteral?: TestValue; + // @ts-expect-error config limited to specific string or valid reactive prop + @wire(TestAdapter, { config: 'incorrect' }) + wrongConfigButInferredAsString?: 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 {}; + // @ts-expect-error config now allowed + @wire(TestAdapterNoConfig, {}) + forcingConfigOnAdapter?: 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(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 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; + // @ts-expect-error config limited to specific string or valid reactive prop + @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 +215,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) @@ -210,16 +284,85 @@ export class MethodDecorators extends LightningElement { // 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. + // @ts-expect-error config limited to specific string or valid reactive prop @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) {} + + // make sure imperatively calling works correctly + // no support for reactive props so testing is simpler + imperativeTest(): TestValue { + let output: TestValue; + output = TestAdapterWithImperative({ config: 'config' }); + // @ts-expect-error no reactive props + output = TestAdapterWithImperative({ config: '$configProp' }); + // @ts-expect-error extra config prop + output = TestAdapterWithImperative({ config: 'config', extra: 'val' }); + // @ts-expect-error missing config prop + output = TestAdapterWithImperative({}); + // @ts-expect-error missing param + output = TestAdapterWithImperative(); + return output; + } +} + +/** 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 {} + // @ts-expect-error config limited to specific string or valid reactive prop + @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 */ @@ -244,7 +387,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 +484,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 +561,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 +617,91 @@ 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) {} +} + +/** Ensure that components extending other components correctly use the hosting component's props */ +export class BaseComponent 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; +} + +export class ExtensionComponent extends BaseComponent { + // --- VALID --- // + // Valid - basic + @wire(TestAdapter, { config: 'config' }) + basic?: TestValue; + @wire(TestAdapter, { config: '$configProp' }) + simpleReactive?: TestValue; + @wire(TestAdapter, { config: '$nested.prop' }) + nestedReactive?: TestValue; + + // --- INVALID --- // + // @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; +} From 0a0e67567584fc5813b1996d5536ed477667a81a Mon Sep 17 00:00:00 2001 From: Heber Date: Tue, 7 Jan 2025 08:41:06 -0700 Subject: [PATCH 2/3] Accept recommended changes `ReactivePropertyOfComponent` Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- packages/@lwc/engine-core/src/framework/wiring/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/engine-core/src/framework/wiring/types.ts b/packages/@lwc/engine-core/src/framework/wiring/types.ts index 30d9df2f7b..d3c81aa376 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/types.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/types.ts @@ -95,7 +95,7 @@ export type RegisterContextProviderFn = ( * * when looking for compatible types mark all optional properties as required so we can correctly evaluate their resolved types */ -type ReactivePropertyOfCompoent = PrefixDollarSign< +type ReactivePropertyOfComponent = PrefixDollarSign< PropertiesOfType>, ExpectedType> >; From ac422bede479f29feb3526855946bfdc892807e6 Mon Sep 17 00:00:00 2001 From: Heber Date: Tue, 7 Jan 2025 08:41:48 -0700 Subject: [PATCH 3/3] Accept changes in string pattern for prefixing Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- packages/@lwc/engine-core/src/framework/wiring/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/engine-core/src/framework/wiring/types.ts b/packages/@lwc/engine-core/src/framework/wiring/types.ts index d3c81aa376..38675bbfbd 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/types.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/types.ts @@ -100,7 +100,7 @@ type ReactivePropertyOfComponent = PrefixDollarSign< >; /** utility type */ -type PrefixDollarSign = `${'$'}${T}`; +type PrefixDollarSign = `$${T}`; /** recursively find all properties on the target that are of a compatible type, returning their paths as strings */ type PropertiesOfType = {