Skip to content
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

Make TS Discriminating Unions Props Possible #9130

Closed
arkmech opened this issue Aug 21, 2023 · 15 comments
Closed

Make TS Discriminating Unions Props Possible #9130

arkmech opened this issue Aug 21, 2023 · 15 comments

Comments

@arkmech
Copy link

arkmech commented Aug 21, 2023

Describe the problem

<script context="module" lang="ts">
  export type Props = {
    name: string;
  } & (
    | { iconType?: 'default'; icon?: typeof SvelteComponent }
    | {
        iconType?: 'featured';
        icon?: typeof SvelteComponent;
        featuredIconProps?: FeaturedIconProps;
      }
  );
</script>

<script lang="ts">
  type $$Props = Props;

  export let name: $$Props['name'];
  export let iconType: $$Props['iconType'] = undefined;
  export let icon: $$Props['icon'] = undefined;
</script>

{#if iconType === 'default'}
  <svelte:component this={icon} />
{:else if iconType === 'featured'}
  <FeaturedIcon {icon} {...featuredIconProps}  />
{/if}

It doesn't seem possible to access unique props from discriminated unions

Describe the proposed solution

Attempted to access a unique prop.

<script lang="ts">
  let props: $$Props;
  if (iconType === 'featured') {
    props. // featuredIconProps doesn't show up.
  }
</script>

Alternatives considered

None

Importance

i cannot use svelte without it

@dummdidumm
Copy link
Member

Please provide a proper reproduction. You code snippets are missing some code or contain bugs - for example, where is iconType coming from?

@arkmech
Copy link
Author

arkmech commented Aug 21, 2023

Please provide a proper reproduction. You code snippets are missing some code or contain bugs - for example, where is iconType coming from?

Updated with iconType exported

@arkmech
Copy link
Author

arkmech commented Aug 22, 2023

Please provide a proper reproduction. You code snippets are missing some code or contain bugs - for example, where is iconType coming from?

Reproduction https://github.com/arkmech/svelte-discriminating-union

@TomDo1234
Copy link

This is something React has that Svelte does not

@dummdidumm dummdidumm added this to the 5.x milestone Aug 22, 2023
@amit13k
Copy link

amit13k commented Aug 22, 2023

Please provide a proper reproduction. You code snippets are missing some code or contain bugs - for example, where is iconType coming from?

Reproduction https://github.com/arkmech/svelte-discriminating-union

Not sure if solves all the issues but this works.

<script context="module" lang="ts">
	type Props = {
		name: string;
	} & (
		| { color?: 'red' }
		| {
				color: 'green';
				uniqueProp?: string;
		  }
	);
</script>

<script lang="ts">
	type $$Props = Props;
	const props = $$props as $$Props;
</script>

<div>{props.name}</div>
{#if props.color === 'red'}
	<div>Red</div>
{:else if props.color === 'green'}
	<div>Green</div>
	<div>{props.uniqueProp}</div>
{/if}

@CaptainCodeman
Copy link

CaptainCodeman commented Aug 22, 2023

IMO it's already easy to use discriminated unions in Svelte, here's an example (imagine a blog admin page, needing to show the content for a URL, which could be a file download, an article page, or a redirect to another URL):

<script lang="ts">
  import type { Content } from '$lib/models'
  import ContentDownload from './ContentDownload.svelte'
  import ContentPage from './ContentPage.svelte'
  import ContentRedirect from './ContentRedirect.svelte'
  import ContentUnknown from './ContentUnknown.svelte'

  export let content: Content

  function component(content: Content) {
    switch (content.type) {
      case 'Page':
        return ContentPage
      case 'Redirect':
        return ContentRedirect
      case 'Download':
        return ContentDownload
      default:
        return ContentUnknown
    }
  }
</script>

<svelte:component this={component(content)} {content} />

Each type-specific component gets the strongly typed content to do whatever it needs with it.

@TomDo1234
Copy link

IMO it's already easy to use discriminated unions in Svelte, here's an example (imagine a blog admin page, needing to show the content for a URL, which could be a file download, an article page, or a redirect to another URL):

<script lang="ts">
  import type { Content } from '$lib/models'
  import ContentDownload from './ContentDownload.svelte'
  import ContentPage from './ContentPage.svelte'
  import ContentRedirect from './ContentRedirect.svelte'
  import ContentUnknown from './ContentUnknown.svelte'

  export let content: Content

  function component(content: Content) {
    switch (content.type) {
      case 'Page':
        return ContentPage
      case 'Redirect':
        return ContentRedirect
      case 'Download':
        return ContentDownload
      default:
        return ContentUnknown
    }
  }
</script>

<svelte:component this={component(content)} {content} />

Each type-specific component gets the strongly typed content to do whatever it needs with it.

Yes but what we mean is type discrimination on "top level", not "Make every prop and wrap it into an object prop, then type discriminate that"

@TomDo1234
Copy link

Please provide a proper reproduction. You code snippets are missing some code or contain bugs - for example, where is iconType coming from?

Reproduction https://github.com/arkmech/svelte-discriminating-union

Not sure if solves all the issues but this works.

<script context="module" lang="ts">
	type Props = {
		name: string;
	} & (
		| { color?: 'red' }
		| {
				color: 'green';
				uniqueProp?: string;
		  }
	);
</script>

<script lang="ts">
	type $$Props = Props;
	const props = $$props as $$Props;
</script>

<div>{props.name}</div>
{#if props.color === 'red'}
	<div>Red</div>
{:else if props.color === 'green'}
	<div>Green</div>
	<div>{props.uniqueProp}</div>
{/if}

Yes that "works" but you get type linting when you actually use the component and put props on it on the parent component.

@amit13k
Copy link

amit13k commented Aug 23, 2023

Yes that "works" but you get type linting when you actually use the component and put props on it on the parent component.

Hmm, are you saying type linting doesn't work when using this component ?. You do get type errors when you use the component. Here is a REPL example, https://www.sveltelab.dev/0ds6ozd652e1o1e

@CaptainCodeman
Copy link

Yes but what we mean is type discrimination on "top level", not "Make every prop and wrap it into an object prop, then type discriminate that"

For what purpose? If you're using Typescript discriminated types in a project wouldn't it make more sense to ... well, use them? Why start splitting apart the props to pass them separately when you more than likely already have everything as an object anyway (the whole point of discriminated unions). Why wouldn't you pass that object?

And if you "must" do it in a single component, you can add a little type casting to get the props:

<script lang="ts" context="module">
  type NetworkLoadingState = {
    state: "loading";
  };

  type NetworkFailedState = {
    state: "failed";
    code: number;
  };

  type NetworkSuccessState = {
    state: "success";
    response: {
      title: string;
      duration: number;
      summary: string;
    };
  };

  // Create a type which represents only one of the above types
  // but you aren't sure which it is yet.
  type NetworkState =
    | NetworkLoadingState
    | NetworkFailedState
    | NetworkSuccessState;
</script>

<script lang="ts">
  export let value: NetworkState

  const asLoading = (value: NetworkState) => value as NetworkLoadingState
  const asFailed = (value: NetworkState) => value as NetworkFailedState
  const asSuccess = (value: NetworkState) => value as NetworkSuccessState
</script>

{#if value.state === 'loading'}
  {@const loading = asLoading(value)}
  {loading.state}
{/if}

{#if value.state === 'failed'}
  {@const failed = asFailed(value)}
  {failed.state}
  {failed.code}
{/if}

{#if value.state === 'success'}
  {@const success = asSuccess(value)}
  {success.state}
  {success.response.title}
  {success.response.duration}
  {success.response.summary}
{/if}

@robertadamsonsmith
Copy link

Using a more straight forward example, to make the issue clearer, and echoing other comments.

If you need to type $$Props directly like this, so that the components props can vary, it is most probably a good idea to then just use $$props. You get full typescript safety from within and outside your component this way:

<script lang="ts">
	type Circle = { shape: 'circle'; radius: number };
	type Rectangle = { shape: 'rectangle'; width: number; height: number };
	type $$Props = Circle | Rectangle;

	$:props = $$props as $$Props;
</script>

{#if props.shape==="circle"}
	<div>Circle, radius = {props.radius}</div>
{:else}
	<div>Rectangle, width = {props.width}, height = {props.height}</div>
{/if}

The only issue, is that it would be better ergonomics if $$props was already typed to $$Props, so that you could simply use $$props directly, and avoid having to write the props^3 line:

	$:props = $$props as $$Props;

@arkmech
Copy link
Author

arkmech commented Aug 25, 2023

Thank you everyone that has responded so far.
It seems that this is the key:
props = $$props as $$Props;

External Component API works
Internal - No Errors.

When accessing properties on props, there is no TS intellisense. Let me know if anyone else gets no intellisense on props

@robertadamsonsmith
Copy link

Using a regular sveltekit project with typescript enabled, and visual studio code with the svelte for vs code extension, you should get full intellisense internally

props3

and externally

props3-2

@amit13k
Copy link

amit13k commented Aug 27, 2023

Also note that, for the intellisense to work, the {#if} block should already have {/if}. Also {props. without closing } won't work for intellisense.

@dummdidumm
Copy link
Member

Svelte 5 will fix this through the $props() rune, which takes a generic for the parameters. You can easily type the discriminated unions there and use the props object then. TS playground example

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

No branches or pull requests

7 participants