Skip to content

Svelte5: unexpected Component type errors #12627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
FoHoOV opened this issue Jul 26, 2024 · 4 comments · Fixed by #12666
Closed

Svelte5: unexpected Component type errors #12627

FoHoOV opened this issue Jul 26, 2024 · 4 comments · Fixed by #12666

Comments

@FoHoOV
Copy link
Contributor

FoHoOV commented Jul 26, 2024

Describe the bug

If you have a component (lets call it First), the followings will result in a type error:

import First from '$lib/components/First.svelte';
const COMPONENTS = {
	first: First
}
type GetComponent<TKey extends keyof typeof COMPONENTS> = (typeof COMPONENTS)[TKey]; 

// error
type FirstProps = ComponentProps<GetComponent<'first'>>;

function doSomething<TComponent extends Component>(component: TComponent) {}
// error
doSomething(COMPONENTS["first"]);

for the last one, if you do something like this instead, it will work though:

function doSomething2<TComponent extends Component<any, any, any>>(component: TComponent) {}
// no errors here
doSomething2(COMPONENTS["first"]);

Another usage example:

<script lang="ts">
	import type FirstType from '$lib/components/First.svelte';
        import First from '$lib/components/First.svelte';

	type ComponentMap = {
		first: FirstType;
	};
	let first: ComponentMap['first'] = First;
</script>

<!-- using first here results in a typeerror -->
<svelte:component this={first}
></svelte:component>

I might be using it wrong, but as a replacement for SvelteComponent its giving me issues.

Reproduction

read description

Logs

No response

System Info

System:
    OS: Linux 5.15 Ubuntu 22.04.4 LTS 22.04.4 LTS (Jammy Jellyfish)
    CPU: (16) x64 AMD Ryzen 7 4800H with Radeon Graphics
    Memory: 10.23 GB / 15.33 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 22.2.0 - ~/.nvm/versions/node/v22.2.0/bin/node
    npm: 10.7.0 - ~/.nvm/versions/node/v22.2.0/bin/npm
    pnpm: 9.4.0 - ~/.nvm/versions/node/v22.2.0/bin/pnpm
  Browsers:
    Chrome: 123.0.6312.105
  npmPackages:
    svelte: 5.0.0-next.199 => 5.0.0-next.199

Severity

annoyance

@FoHoOV FoHoOV changed the title Svelte5: Use Component type to pass component as function argument is a type error Svelte5: Component type errors Jul 26, 2024
@FoHoOV FoHoOV changed the title Svelte5: Component type errors Svelte5: unexpected Component type errors Jul 26, 2024
@FoHoOV
Copy link
Contributor Author

FoHoOV commented Jul 27, 2024

I'm creating a small repo that contains what works and should continue to work and what I expected to work (imo), but doesn't.

@FoHoOV
Copy link
Contributor Author

FoHoOV commented Jul 27, 2024

Title.svelte

<script lang="ts">
	import { onMount } from 'svelte';

	const { title }: { title: string } = $props();
	let _isMounted = false;

	export function isMounted() {
		return _isMounted;
	}

	onMount(() => {
		_isMounted = true;
	});
</script>

<h1>{title}</h1>

doesn't work (all of these are type errors)

<script lang="ts">
	import type TitleType from '$lib/components/Title.svelte';
	import Title from '$lib/components/Title.svelte';
	import type { Component, ComponentProps } from 'svelte';

	type ComponentMap = {
		title: TitleType;
	};
	const inferredMap = {
		title: Title
	};

	const renamedFromTypeMap: ComponentMap['title'] = Title;
	const fromFunction = getFromFunction(Title);
	const fromFunctionWithAny = getFromFunctionWithAny(Title);
	const fromFunctionInferred = getFromFunction(inferredMap.title);
	const props: ComponentProps<typeof Title> = {
		title: 'test'
	};
	doSomething(Title, { title: 'something' });
	doSomething<TitleType>(Title, { title: 'something' });
	doSomething<typeof Title>(Title, { title: 'something' });
	doSomething<ComponentMap['title']>(Title, { title: 'something' });

	function getFromFunction<T extends Component>(component: T) {
		return component;
	}

	// in the docs for ComponentProps we see:
	// * function withProps<TComponent extends Component<any>>(
	//  * 	component: TComponent,
	//  * 	props: ComponentProps<TComponent>
	//  * ) {};
	// but it still doesnt work, <any, any, any> works tho
	function getFromFunctionWithAny<T extends Component<any>>(component: T) {
		return component;
	}

	function doSomething<TComponent extends Component>(
		component: TComponent,
		props: ComponentProps<TComponent>
	) {
		return component;
	}
</script>

works and should continue to work (imo)

<script lang="ts">
	import Title from '$lib/components/Title.svelte';
	import type TitleType from '$lib/components/Title.svelte';
	import type { Component, ComponentProps } from 'svelte';

	const renamedTitle = Title;
	const inferredMap = {
		title: Title
	};

	type ComponentMap = {
		title: typeof Title;
	};
	const renamedTitleFromTypeMap: ComponentMap['title'] = Title;
	const renamedTitleFromVar: ComponentMap['title'] = renamedTitle;
	const fromFunction = getFromFunction(Title);
	const fromFunctionWithMap = getFromFunction(inferredMap.title);
	const props: ComponentProps<Title> = {
		title: 'test',
		// @ts-expect-error - its not a prop
		isMounted() {
			return true;
		}
	};
	const props2: ComponentProps<TitleType> = {
		title: 'test'
	};

	function getFromFunction<TComponent extends Component<any, any, any>>(component: TComponent) {
		return component;
	}
</script>

@dummdidumm dummdidumm added awaiting submitter needs a reproduction, or clarification and removed awaiting submitter needs a reproduction, or clarification labels Jul 30, 2024
@dummdidumm
Copy link
Member

This mostly works as expected, there are a few mistakes in your code:

  • import type Component from './Component.svelte' means you get the instance type, not the constructor type. Doing import Component from './Component.svelte' and then doing typeof Component is the better way
  • The Component type is restrictive by default to prevent bugs. The result is that you need to use the <any, any> generics in more places (like you already did in a few others) to say "any kind of prop / export shape is allowed"

Here's the corrected code:

<script lang="ts">
	import Title from './Title.svelte';
	import type { Component, ComponentProps} from 'svelte';

	type ComponentMap = {
		title: typeof Title;
	};
	const inferredMap = {
		title: Title
	};

	const renamedFromTypeMap: ComponentMap['title'] = Title;
	const fromFunction = getFromFunction(Title);
	const fromFunctionWithAny = getFromFunctionWithAny(Title);
	const fromFunctionInferred = getFromFunction(inferredMap.title);
	const props: ComponentProps<typeof Title> = {
		title: 'test'
	};
	doSomething(Title, { title: 'something' });
	doSomething<typeof Title>(Title, { title: 'something' });
	doSomething<ComponentMap['title']>(Title, { title: 'something' });

	function getFromFunction<T extends Component<any, any>>(component: T) {
		return component;
	}

	// in the docs for ComponentProps we see:
	// * function withProps<TComponent extends Component<any>>(
	//  * 	component: TComponent,
	//  * 	props: ComponentProps<TComponent>
	//  * ) {};
	// but it still doesnt work, <any, any, any> works tho
	function getFromFunctionWithAny<T extends Component<any, any>>(component: T) {
		return component;
	}

	function doSomething<TComponent extends Component<any, any>>(
		component: TComponent,
		props: ComponentProps<TComponent>
	) {
		return component;
	}
</script>

That said, there's one bug in the type of ComponentProps which we need to fix

dummdidumm added a commit that referenced this issue Jul 30, 2024
language tools has to type its own shape for backwards compatibility, and it currently doesn't include the `$on` and `$set` methods, which means without widening the type as done here you would get a "this shape is not accepted" type error when passing it to `ComponentProps`

closes #12627
dummdidumm added a commit that referenced this issue Jul 30, 2024
language tools has to type its own shape for backwards compatibility, and it currently doesn't include the `$on` and `$set` methods, which means without widening the type as done here you would get a "this shape is not accepted" type error when passing it to `ComponentProps`

closes #12627
Rich-Harris pushed a commit that referenced this issue Jul 30, 2024
#12666)

language tools has to type its own shape for backwards compatibility, and it currently doesn't include the `$on` and `$set` methods, which means without widening the type as done here you would get a "this shape is not accepted" type error when passing it to `ComponentProps`

closes #12627
@FoHoOV
Copy link
Contributor Author

FoHoOV commented Jul 30, 2024

import type Component from './Component.svelte' means you get the instance type, not the constructor type. Doing import Component from './Component.svelte' and then doing typeof Component is the better way

@dummdidumm I'm concerned about two things here:

  1. If we think how ts works, they should be identical -- import type X == import X -> typeof X, am I wrong? If not, do you think it should be documented? specially now that all components are functions not classes in svelte-5 thinking about constructors is a bit weird no?
  2. about treeshaking. If I import the type, it will be removed after ts compiles, but importing it and then using typeof X might not remove it after a build. I'm not sure tho, if we don't use the imported thing directly, it should be tree-shaken away, so 99% I'm wrong (also I think it depends on the build-system/bundler, but import type doesn't and gets removed after TS).

Edit: Also to get the types of exported functions on component we have to do InstanceType<Component> whilest we don't think of components as classes anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants