Skip to content

Commit

Permalink
Sadly, due to a combination of requirements and TypeScript limitation…
Browse files Browse the repository at this point in the history
…s, we need to always create both a legacy class component and function component type.

 - Constraints: Need to support Svelte 4 class component types, therefore we need to use __sveltets_2_ensureComponent to transform function components to classes
- Limitations: TypeScript is not able to preserve generics during said transformation (i.e. there's no way to express keeping the generic etc)
TODO Svelte 6/7: Switch this around and not use new Component in svelte2tsx anymore, which means we can remove the legacy class component. We need something like _ensureFnComponent then.
  • Loading branch information
dummdidumm committed Sep 25, 2024
1 parent 96d10a7 commit e3fd0b0
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts" generics="T">
let { readonly, can_bind = $bindable() }: { readonly?: T; can_bind?: T } = $props();
export function only_bind() {
return true;
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,73 @@
"severity": 1,
"source": "ts",
"tags": []
},
{
"code": 2322,
"message": "Cannot use 'bind:' with this property. It is declared as non-bindable inside the component.\nTo mark a property as bindable: 'let { readonly = $bindable() } = $props()'",
"range": {
"end": {
"character": 27,
"line": 29
},
"start": {
"character": 14,
"line": 29
}
},
"severity": 1,
"source": "ts",
"tags": []
},
{
"code": 2353,
"message": "Object literal may only specify known properties, and 'only_bind' does not exist in type '$$ComponentProps'.",
"range": {
"end": {
"character": 28,
"line": 30
},
"start": {
"character": 19,
"line": 30
}
},
"severity": 1,
"source": "ts",
"tags": []
},
{
"code": 2322,
"message": "Cannot use 'bind:' with this property. It is declared as non-bindable inside the component.\nTo mark a property as bindable: 'let { only_bind = $bindable() } = $props()'",
"range": {
"end": {
"character": 28,
"line": 30
},
"start": {
"character": 14,
"line": 30
}
},
"severity": 1,
"source": "ts",
"tags": []
},
{
"code": 2353,
"message": "Object literal may only specify known properties, and 'only_bind' does not exist in type '$$ComponentProps'.",
"range": {
"end": {
"character": 24,
"line": 31
},
"start": {
"character": 15,
"line": 31
}
},
"severity": 1,
"source": "ts",
"tags": []
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
let can_bind = '';
let readonly = ''
let instance: Runes;
let instance: ReturnType<typeof Runes>;
instance!.only_bind() === true;
</script>

Expand All @@ -26,3 +26,7 @@
<Runes bind:readonly />
<Runes bind:only_bind />
<Runes {only_bind} />

<RunesGeneric bind:readonly />
<RunesGeneric bind:only_bind />
<RunesGeneric {only_bind} />
37 changes: 15 additions & 22 deletions packages/svelte2tsx/src/svelte2tsx/addComponentExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,28 +109,21 @@ ${
: `${returnType('props')} & {${eventsSlotsType.join(', ')}}`;
const bindingsType = `ReturnType<__sveltets_Render${generics.toReferencesAnyString()}['bindings']>`;

if (exportedNames.usesRunes() && !events.hasEvents() && !usesSlots) {
statement +=
`\ntype $$$Component${genericsDef} = import('svelte').Component<${propsType}, ${returnType('exports')}, ${bindingsType}> \n` +
`${doc}declare function ${className || '$$Component'}${genericsDef}(...args: Parameters<$$$Component${generics.toReferencesString()}>): ReturnType<$$$Component${generics.toReferencesString()}>;\n` +
// The only way to preserve the generic type when transforming this function type to a class via __sveltets_2_ensureComponent later is a function,
// and so we need to manually add the properties on the function itself this way.
`${className || '$$Component'}.z_$$bindings = null as any as ${bindingsType};\n` +
`${className || '$$Component'}.element = null as any;\n` +
`export default ${className || '$$Component'};`;
} else {
statement +=
`\ninterface $$IsomorphicComponent {\n` +
` new ${genericsDef}(options: import('svelte').ComponentConstructorOptions<${returnType('props') + (usesSlots ? '& {children?: any}' : '')}>): import('svelte').SvelteComponent<${returnType('props')}, ${returnType('events')}, ${returnType('slots')}> & { $$bindings?: ${returnType('bindings')} } & ${returnType('exports')};\n` +
` ${genericsDef}(internal: unknown, props: ${propsType}): ${returnType('exports')};\n` +
` z_$$bindings?: ${bindingsType};\n` +
`}\n` +
`${doc}const ${className || '$$Component'}: $$IsomorphicComponent = null as any;\n` +
surroundWithIgnoreComments(
`type ${className || '$$Component'}${genericsDef} = InstanceType<typeof ${className || '$$Component'}${genericsRef}>;\n`
) +
`export default ${className || '$$Component'};`;
}
// Sadly, due to a combination of requirements and TypeScript limitations, we need to always create both a legacy class component and function component type.
// - Constraints: Need to support Svelte 4 class component types, therefore we need to use __sveltets_2_ensureComponent to transform function components to classes
// - Limitations: TypeScript is not able to preserve generics during said transformation (i.e. there's no way to express keeping the generic etc)
// TODO Svelte 6/7: Switch this around and not use new Component in svelte2tsx anymore, which means we can remove the legacy class component. We need something like _ensureFnComponent then.
statement +=
`\ninterface $$IsomorphicComponent {\n` +
` new ${genericsDef}(options: import('svelte').ComponentConstructorOptions<${returnType('props') + (usesSlots ? '& {children?: any}' : '')}>): import('svelte').SvelteComponent<${returnType('props')}, ${returnType('events')}, ${returnType('slots')}> & { $$bindings?: ${returnType('bindings')} } & ${returnType('exports')};\n` +
` ${genericsDef}(internal: unknown, props: ${propsType}): ${returnType('exports')};\n` +
` z_$$bindings?: ${bindingsType};\n` +
`}\n` +
`${doc}const ${className || '$$Component'}: $$IsomorphicComponent = null as any;\n` +
surroundWithIgnoreComments(
`type ${className || '$$Component'}${genericsDef} = InstanceType<typeof ${className || '$$Component'}${genericsRef}>;\n`
) +
`export default ${className || '$$Component'};`;
} else if (mode === 'dts') {
statement +=
`export type ${PropsName}${genericsDef} = ${returnType('props')};\n` +
Expand Down
33 changes: 29 additions & 4 deletions packages/svelte2tsx/svelte-shims-v4.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,36 @@ declare type ATypedSvelteComponent = {
* ```
*/
declare type ConstructorOfATypedSvelteComponent = new (args: {target: any, props?: any}) => ATypedSvelteComponent
// Usage note: Cannot properly transform generic function components to class components due to TypeScript limitations
declare function __sveltets_2_ensureComponent<
// @ts-ignore svelte.Component doesn't exist in Svelte 4
T extends ConstructorOfATypedSvelteComponent | (typeof import('svelte') extends { mount: any } ? import('svelte').Component<any, any, any> : never) | null | undefined
// @ts-ignore svelte.Component doesn't exist in Svelte 4
>(type: T): NonNullable<T extends ConstructorOfATypedSvelteComponent ? T : typeof import('svelte') extends { mount: any } ? T extends import('svelte').Component<infer Props extends Record<string, any>> ? typeof import('svelte').SvelteComponent<Props, Props['$$events'], Props['$$slots']> : T : T>;
T extends
| ConstructorOfATypedSvelteComponent
| (typeof import('svelte') extends { mount: any }
? // @ts-ignore svelte.Component doesn't exist in Svelte 4
import('svelte').Component<any, any, any>
: never)
| null
| undefined
>(
type: T
): NonNullable<
T extends ConstructorOfATypedSvelteComponent
? T
: typeof import('svelte') extends { mount: any }
? // @ts-ignore svelte.Component doesn't exist in Svelte 4
T extends import('svelte').Component<
infer Props extends Record<string, any>,
infer Exports extends Record<string, any>,
infer Bindings extends string
>
? new (
options: import('svelte').ComponentConstructorOptions<Props>
) => import('svelte').SvelteComponent<Props, Props['$$events'], Props['$$slots']> &
Exports & { $$bindings: Bindings }
: never
: never
>;

declare function __sveltets_2_ensureArray<T extends ArrayLike<unknown> | Iterable<unknown>>(array: T): T extends ArrayLike<infer U> ? U[] : T extends Iterable<infer U> ? Iterable<U> : any[];

type __sveltets_2_PropsWithChildren<Props, Slots> = Props &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ declare class __sveltets_Render<T extends Record<string, any>, K extends keyof T
baz: () => T;
};
}
type $$$Component<T extends Record<string, any>, K extends keyof T> = import('svelte').Component<ReturnType<__sveltets_Render<T, K>['props']> & {}, ReturnType<__sveltets_Render<T, K>['exports']>, ReturnType<__sveltets_Render<any, any>['bindings']>>;
declare function TestRunes<T extends Record<string, any>, K extends keyof T>(...args: Parameters<$$$Component<T, K>>): ReturnType<$$$Component<T, K>>;
declare namespace TestRunes {
var z_$$bindings: "bar";
var element: any;
interface $$IsomorphicComponent {
new <T extends Record<string, any>, K extends keyof T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, K>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, K>['props']>, ReturnType<__sveltets_Render<T, K>['events']>, ReturnType<__sveltets_Render<T, K>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T, K>['bindings']>;
} & ReturnType<__sveltets_Render<T, K>['exports']>;
<T extends Record<string, any>, K extends keyof T>(internal: unknown, props: ReturnType<__sveltets_Render<T, K>['props']> & {}): ReturnType<__sveltets_Render<T, K>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
}
declare const TestRunes: $$IsomorphicComponent;
type TestRunes<T extends Record<string, any>, K extends keyof T> = InstanceType<typeof TestRunes<T, K>>;
export default TestRunes;
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ class __sveltets_Render<T> {
exports() { return {}; }
}

type $$$Component<T> = import('svelte').Component<ReturnType<__sveltets_Render<T>['props']> & {}, ReturnType<__sveltets_Render<T>['exports']>, ReturnType<__sveltets_Render<any>['bindings']>>
declare function Input__SvelteComponent_<T>(...args: Parameters<$$$Component<T>>): ReturnType<$$$Component<T>>;
Input__SvelteComponent_.z_$$bindings = null as any as ReturnType<__sveltets_Render<any>['bindings']>;
Input__SvelteComponent_.element = null as any;
export default Input__SvelteComponent_;
interface $$IsomorphicComponent {
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & { $$bindings?: ReturnType<__sveltets_Render<T>['bindings']> } & ReturnType<__sveltets_Render<T>['exports']>;
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
const Input__SvelteComponent_: $$IsomorphicComponent = null as any;
/*Ωignore_startΩ*/type Input__SvelteComponent_<T> = InstanceType<typeof Input__SvelteComponent_<T>>;
/*Ωignore_endΩ*/export default Input__SvelteComponent_;

0 comments on commit e3fd0b0

Please sign in to comment.