svelte event parameter type for typescript

25.3k views Asked by At

So new to svelte but it is so small it is perfect for a job i am working on.

Went for the typescript option: https://svelte.dev/blog/svelte-and-typescript

How or where can i find the types for custom component events:

A simple login component form:

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

  const dispatch = createEventDispatcher()
  let isSubmitting = false
  const handleSubmit = (e: HTMLFormElement) => {
    e.preventDefault()
    isSubmitting = true
    const payload = {
      username: e.target.username.value,
      password: e.target.password.value,
    }
    dispatch('submit', payload)
  }
</script>

<form on:submit={handleSubmit}>
    <label for="username"><b>Username</b></label>
    <input type="text" placeholder="Enter Username" name="username" required id="username">

    <label for="password"><b>Password</b></label>
    <input type="password" placeholder="Enter Password" name="password" required id="password">

    <button type="submit" disabled={isSubmitting}>Login</button>
</form>

Included in another component to handle the submit:

<script lang="ts">
  import Login from './molecules/Login.svelte'
  const loginHandle = function (a: any) {
    console.log(a)
  }
</script>

<main class="{open}">
   {#if !authenticated}
      <Login on:submit={loginHandle}/>
   {/if}
</main>

Right now there is an ugly any added to the loginHandle but when dumping the event to console it looks to be very svelte specific.. where can i find the type?

5

There are 5 answers

1
Roman Mahotskyi On

How or where can i find the types for custom component events:

You can pass an object to the generic of the createEventDispatcher function. Where key is a custom event name and value is a detail object.

Child component (<Calendar />)

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

  const dispatch = createEventDispatcher<{ select: Date }>()

  function onSelect(date: Date): void {
    dispatch('select', date)
  }
</script>

...

Parent component

<script>
  function handleSelect (event: CustomEvent<Date>) {
    console.log('Selected date', event.detail)
  }
</script>

<Calendar on:select={handleSelect} />

Usage example

enter image description here

2
rodgco On

Svelte now ships with a CustomEvent interface definition. So you can declare your function as below.

const loginHandle = function (a: CustomEvent) {
    console.log(a)
}
1
DoomGoober On

For full typing, change the event raiser to:

const dispatch = createEventDispatcher<{submit:{username:string, password:string}}>()

And the event consumer to:

const loginHandle = function (a: CustomEvent<{username:string, password:string}>) {
    console.log(a.detail.username) //username is type string
    console.log(a.detail.password) //password is type string
}

This will make dispatch("submit", "wrongDetailType") fail. And it eliminates the a.detail being of type "any" in the handler.

0
Dois On

Create the type for all the events that your component will dispatch, and create the type details for the CustomEvent's that you'll be dispatching.

Doing it this way allows your component's event dispatcher to be linked to the custom event through the submit detail.

In the emitting component:

<!-- Login Component -->
<script lang="ts" context="module">
    type ComponentEvents = {
        submit: SubmitDetail;
        // add more properties for each event you wish to dispatch
    };

    type SubmitDetail = {
        username: string;
        password: string;
    };

    export type LoginSubmitEvent = CustomEvent<SubmitDetail>;
</script>

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

    const dispatch = createEventDispatcher<ComponentEvents>();

    const handleSubmit = (e: HTMLFormElement) => {

        e?.preventDefault();

        const payload: SubmitDetail = {
            username: e?.target?.username?.value,
            password: e?.target?.password?.value,
        }

        dispatch('submit', payload)
    }
</script>

<form on:submit={handleSubmit}>
    <label for="username"><b>Username</b></label>
    <input type="text" placeholder="Enter Username" name="username" required id="username">

    <label for="password"><b>Password</b></label>
    <input type="password" placeholder="Enter Password" name="password" required id="password">

    <button type="submit" disabled={isSubmitting}>Login</button>
</form>

In the consuming component:

<!-- Another component that's using the Login Component -->
<script lang="ts">
    import Login from "./Login.svelte"
    import type { LoginSubmitEvent } from "./Login.svelte"
    
    function handleLogin (event: LoginSubmitEvent) {
        console.log("Username:", event?.detail?.username);
        console.log("Password:", event?.detail?.password);
    }
</script>

<Login on:submit={handleLogin}/>

If you need to tie the handler and dispatcher together - you'll have to do it via exporting the callback function to be defined by the calling component, which you can follow Kevin Cox's example for... However, that doesn't answer this question. To decide on whether you should use custom events or callbacks, you can refer to this official discussion: https://github.com/sveltejs/svelte/issues/2323#issuecomment-569954265

0
Kevin Cox On

I found that this is not possible using the Svelte event system. You can declare the type on each side but there is nothing at this time to validate that the types (or event the event names) in the component match that in the code using the component.

However this can be worked around using callback properties rather than events. The only downsides that I am aware of is that the onSubmit name is less distinct than on:submit as a callback/event and that you need to declare events that you propagate. Overall this seems like a very small price to pay for type checking on both the event names and event values. You can also make required callbacks which can be useful for components which don't make sense to use without listening to specific events.

Example

This is a minimized example of the code in the original question.

Component

<script context="module" lang="ts">
    export interface Payload {
        username: string,
        password: string,
    }
</script>
<script lang="ts">
    export let onSubmit: (payload: Payload) => void;

    function handleSubmit(e: SubmitEvent) {
        e.preventDefault()
        onSubmit({
            username: e.target.username.value,
            password: e.target.password.value,
        });
    }
</script>

<form on:submit={handleSubmit}>
    <input name=username required>
    <input type=password name=password required>
    <button type=disabled>Login</button>
</form>

Note: To make the handler optional simply set a noop function as a default value:

export let onSubmit: (payload: Payload) => void = () => {};

Caller

<script lang="ts">
    import Login from "./Login.svelte"
    import type {SubmitEvent} from "./Login.svelte"
    function handleLogin (event: SubmitEvent) {
        console.log(event)
    }
</script>

<main>
    <Login onSubmit={handleLogin}/>
</main>