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

Typed slots #192

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Typed slots #192

wants to merge 5 commits into from

Conversation

pikax
Copy link
Member

@pikax pikax commented Jul 22, 2020

Allow to describe slots and types

SFC

<template>
  <div>
    <!-- <slot name="random" /> type error no slot defined -->
    <!-- <slot name="top" v-bind="{a: 1}" /> type error no args expected -->
    <slot name="top" />
    <div v-for="(item, i) in items" :key="i">
      <slot name="item" v-bind="{ item, i }" />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, defineAsyncComponent, h, createSlots } from 'vue'
export default defineComponent({
  slots: {
    top: null, // no value
    item: Object as () => { item: { value: number }; i: number },
  },
  setup() {
    const items = Array.from({ length: 10 }).map((_, value) => ({ value }))
    return {
      items,
    }
  },
})
</script>

Render

export default defineComponent({
  slots: {
    top: null, // no value
    item: Object as () => { item: { value: number }; i: number },
  },
  setup(_, { slots }) {
    const items = Array.from({ length: 10 }).map((_, value) => ({ value }))

    return () => {
      return h('div', [
        //slots.top({a: 1}), // type error no args expected
        //slots.random({a: 1}), // type error no slot defined
        slots.top(),
        items.map((item, i) => h('div', slots.item && slots.item({ item, i }))),
      ])
    }
  },
})

Type only

defineComponent({
  slots: null as {
    // slot name `item`
    item: { value: SlotType<number> }
  },
  // ...
})

Rendered

@posva
Copy link
Member

posva commented Jul 22, 2020

Wouldn't it be better to add it to attributes.json for Vetur so it doesn't bloat the final bundle?

@pikax
Copy link
Member Author

pikax commented Jul 22, 2020

I think it would be better to have automated tools to generate attributes.json and web-types.json, also this would allow to get the types even on a render function using h, something the *.json can't do it.

EDIT: this would also allow us to check at runtime if the user is trying to use a slot that doesn't exist.

@CyberAP
Copy link
Contributor

CyberAP commented Jul 22, 2020

How would you type slots that depend on props?

@pikax
Copy link
Member Author

pikax commented Jul 22, 2020

How would you type slots that depend on props?

You would need to type it the same as props (duplicating it).

@CyberAP
Copy link
Contributor

CyberAP commented Jul 22, 2020

If this is primarily a TypeScript type hinting wouldn't it be better to export slot typing separately?

<script lang="ts">
export type slots = { default: { message: string } }

...
</script>

@sqal
Copy link

sqal commented Jul 22, 2020

👍for typed slots but I am not a fan of having an extra option that will bloat my production build unless it will be possible to remove these declarations by babel plugin?

@pikax
Copy link
Member Author

pikax commented Jul 22, 2020

If this is primarily a TypeScript type hinting wouldn't it be better to export slot typing separately?

Needs to work with defineComponent aka outside of SFC.

👍for typed slots but I am not a fan of having an extra option that will bloat my production build unless it will be possible to remove these declarations by babel plugin?

I'm thinking doing something similar to typed emit, but when writing this RFC thought it would be cool to give an warning like we do with props.

@CyberAP
Copy link
Contributor

CyberAP commented Jul 23, 2020

Needs to work with defineComponent aka outside of SFC.

But how these typings are going to be useful outside of SFC? I thought that the main idea was to provide type hints inside of <template/> block in SFC (since they're used a lot more regularly than render functions).

Also, what prevents template compiler from doing just that? It knows for sure which slots are available from a component, the only thing missing is prop types, but I guess it is doable as well?

<slot :foo="foo as string" />

Related issue: vuejs/core#1359

@pikax
Copy link
Member Author

pikax commented Jul 23, 2020

But how these typings are going to be useful outside of SFC?

Better typing will be useful everywhere where we need to use the slots, this will be on the component who describes the slots and the component who injects the slot.

export default defineComponent({
  slots: {
    top: null, // no value
    item: Object as () => { item: { value: number }; i: number },
  },
  setup(_, { slots }) {
    const items = Array.from({ length: 10 }).map((_, value) => ({ value }))

    return () => {
      return h('div', [
        //slots.top({a: 1}), // type error no args expected
        //slots.random({a: 1}), // type error no slot defined
        slots.top(),
        items.map((item, i) => h('div', slots.item && slots.item({ item, i }))),
      ])
    }
  },
})

I thought that the main idea was to provide type hints inside of <template/> block in SFC (since they're used a lot more regularly than render functions).

The motivation is better Typescript support! That's not only on the template but anywhere we will use component.

Also, what prevents template compiler from doing just that? It knows for sure which slots are available from a component, the only thing missing is prop types, but I guess it is doable as well?

Render function support.

<slot :foo="foo as string" />

Related issue: vuejs/vue-next#1359

I don't believe this has anything to do with that, this RFC is providing types only for slots, that is to provide typescript support on the template.

@donnysim
Copy link

I don't suppose there's a way to solve:

export default defineComponent({
  setup(props, { slots }) {
    return () => {
      return h('div', [
        props.items.map(item => slots[item.name] && slots[item.name]({ item })),
        // or
        props.items.map(item => slots[`${item.name}-cell`] && slots[`${item.name}-cell`]({ item })),
      ])
    }
  },
})

where slots are dynamic?

@pikax
Copy link
Member Author

pikax commented Jul 24, 2020

You can't describe that with typescript :/

@johnsoncodehk
Copy link
Member

I was support slot type check by IDE with no additional options: https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar

But for third party library like vue-router, I can't read the typescript source code to calculate v-slot types, for this case I need to define additional options for my tool like this: https://github.com/johnsoncodehk/volar#v-slot-type-checking

So if have a slots option let typescript can emit the slots types to .d.ts, this is helpful for third party library.

@ShGKme
Copy link

ShGKme commented Oct 9, 2020

Vue 3 already has emits option with list of emitted events. One of two main roles (as I see it) is helping IDE and other development tools.

Vue has props to define props. Now Vue has emits to define events. It sounds very logical and attractive to have slots to define components' slots. Still component interface is: props + events + provide/inject + slots, it would make component interface fully defined.

And about dynamic slot names. This issue is the same as Dynamic event names in emits option.

So, it seems slots are a part of component interface similar to emits with similar problems, and it would be great to have all defined.

@jods4
Copy link

jods4 commented Mar 13, 2021

It makes sense to make components more type-safe by statically typing their slots interface.

Just want to point out that as is it creates more TS confusion.
Why did you type the slot with a function Object as () => slotType instead of re-using the same pattern as props: as PropType<T>? That adds more arbitrary ways to do similar stuff that needs to be learnt by devs.

More generally that's not great DX and should fall in the same umbrella as the discussion you started in #282 for props.

So I 👍 the concept/idea but the fine API details could need some TS love.

@pikax
Copy link
Member Author

pikax commented Mar 13, 2021

Just want to point out that as is it creates more TS confusion.
Why did you type the slot with a function Object as () => slotType instead of re-using the same pattern as props: as PropType<T>? That adds more arbitrary ways to do similar stuff that needs to be learnt by devs.

Naming wise they are different, PropType is as the name states: describes a type of a prop. Reusing it to describe a Slot will cause more confusion, just because they "do similar stuff", does not mean they are the same. Not to mention that there're fields such as validator and default, that don't make sense on Slot context, required would be arguable, but I don't see much usage from a required slots since slots are usually "defaulted"(<slot name="title">Component title</slot>).

Object as () => slotType

That's optional API, probably could be used to make sure the parameter passed to the slot could be runtime validated (but probably doesn't make much sense).

That's a optional API, similar to props, but that's completely optional and you can describe it as an purely typescript type by (vuejs/core#2693):

  defineComponent({
    slots: {} as {
      test: null
      item: { item: { value: number }; i: number }
    },

    setup(_, { slots }) {
      slots.test!()
      slots.item!({
        i: 22,
        item: {
          value: 22
        }
      })
    }
  })

Also other API that could be possible would be a similar to the emits

defineComponent({
  slots: {
    test(){
    },
    item(item: {value: number}) {
    }
  }
})

But the slot can only take one parameter, which will probably confuse some users, and the execution of the function would not be used (besides runtime validation - already mentioned above).

More generally that's not great DX and should fall in the same umbrella as the discussion you started in #282 for props.

That RFC is unrelated to this one.

So I 👍 the concept/idea but the fine API details could need some TS love.

If you have more feedback, please let me know.

@jods4
Copy link

jods4 commented Mar 14, 2021

Naming wise they are different, PropType is as the name states: describes a type of a prop. Reusing it to describe a Slot will cause more confusion, just because they "do similar stuff", does not mean they are the same.

I agree, but if they're similar then they should have similar API.

For example call it SlotType<T>, or create a more generically usable type alias type VueType<T> = PropType<T> that has a non-specific name and that we could use everywhere.

Or do you plan on back-porting that new syntax to type props like that as well? That would work, too.

Not to mention that there're fields such as validator and default, that don't make sense on Slot context, required would be arguable, but I don't see much usage from a required slots since slots are usually "defaulted"(Component title).

That would be PropOptions.
PropType itself is just about the type:
https://github.com/vuejs/vue-next/blob/4a965802e883107a2af00301a59fb7f403b6acf7/packages/runtime-core/src/componentProps.ts#L49-L56

That's optional API, probably could be used to make sure the parameter passed to the slot could be runtime validated (but probably doesn't make much sense).

It probably doesn't make that much sense because the component defines the slot and provides it -- as opposed to props where the component defines the props and users provides values.

The same could be said of the emit typing, though, and it does provide a way to validate its arguments, so I dunno. 🤷‍♂️

That's a optional API, similar to props, but that's completely optional and you can describe it as an purely typescript type by (vuejs/core#2693):

That's nice! I suppose slots: SomeInterface would work just as well, you don't need the empty object, do you?
I wish real props and emits could work like that.

That RFC is unrelated to this one.

Not directly, but I feel like we should have a unified, easy way to do all the typings in TS, which is a bit what #282 is about (I think?) albeit focused on props.

So I 👍 the concept/idea but the fine API details could need some TS love.
If you have more feedback, please let me know.

Now that you've shown me that slots: T is possible I'm taking back that comment and I think it's a nice way to type in TS.

A random thought I had while thinking about those things: do you think attributes.json or web-types.json are the right way to approach an exported contract that tooling/compiler could use to validate a component usage?
Shouldn't we piggy back TS own type definitions?
We could let TS export a .d.ts for each component and that would contain the props, emits, slots... everything that's needed to know about a component interface.

It's just a random idea, I haven't thought this through.

@pikax
Copy link
Member Author

pikax commented Mar 23, 2021

For example call it SlotType<T>, or create a more generically usable type alias type VueType<T> = PropType<T> that has a non-specific name and that we could use everywhere.

Still need to think about it, VueType might be a bit too generic for this, naming wise I'm still not settle or have strong opinions.

It probably doesn't make that much sense because the component defines the slot and provides it -- as opposed to props where the component defines the props and users provides values.

The same could be said of the emit typing, though, and it does provide a way to validate its arguments, so I dunno. 🤷‍♂️

I have the same opinion, I can see it be useful and be useless, I guess that can leave that to the developers, like we do on the emit

That's nice! I suppose slots: SomeInterface would work just as well, you don't need the empty object, do you?

It won't work because of Typescript,. you would need to cast something to your expected type/interface.

I wish real props and emits could work like that.
props are validated at runtime, so an object or array is required, to allow a pure typescript declaration we need to make a few assumptions see Allow assign all attributes as props
emits you should be able to do just typing, but you will get the warning at dev.

Not directly, but I feel like we should have a unified, easy way to do all the typings in TS, which is a bit what #282 is about (I think?) albeit focused on props.

I would like to keep the scope of each RFC as small as possible, props and slots behave differently (runtime checks and no check respectively). The relation is the Typescript, which the #282 is not only a Typescript approach.

A random thought I had while thinking about those things: do you think attributes.json or web-types.json are the right way to approach an exported contract that tooling/compiler could use to validate a component usage?

There's no concise way yet to describe a lot of things, each IDE/PLUGIN has it's own way to declare types, hopefully this RFC will allow us to describe more component related types on the component through typescript.

My goal is: You should be allowed to describe all the necessary things on the component, such as props, slots, emits, etc.

We could let TS export a .d.ts for each component and that would contain the props, emits, slots... everything that's needed to know about a component interface.

Yep, you should be able to do that already, but that's the goal.

@KaelWD
Copy link

KaelWD commented Jun 18, 2021

This probably isn't possible with the current API but generic slot types would also be useful, for example if I have a component that iterates over an array passed to its items prop:

defineComponent(<T extends any>() => ({ // I think this is already used to pass a setup function directly
  props: {
    items: Array as PropType<T[]>
  },
  slots: null as {
    item: SlotType<{ item: T }>
  },
  setup (props, { slots }) {
    return () => (
      <ul>{
        props.items.map(item => (
          <li>{ slots.item({ item }) }</li>
        ))
      }</ul>
    )
  }
}))

Then using it could be completely safe:

<my-list :items="items">
  <template #item="{ item }">
    {{ item.bar }} <!-- Property 'bar' does not exist on type '{ foo: string; }' -->
  </template>
</my-list>

<script>
export default {
  data: () => ({
    items: [{ foo: '' }]
  })
}
</script>

@jods4
Copy link

jods4 commented Jun 18, 2021

@KaelWD that's a timely comment! Recently I've been trying Volar that typechecks your templates and I had a lot of issues similar to your example.

When I have a bit of time I'm gonna open a discussion in vue-rfcs to argue that we need generic components. Slots is one part of it, but even simple props need it, otherwise you get lots of errors in non-basic scenarios.

It's gonna be challenging but I believe it's the only way if we want type-safe templates.

@KaelWD
Copy link

KaelWD commented Jun 19, 2021

Oh yeah definitely, we have stuff like this too:

defineComponent(<T extends object>() => ({
  props: {
    items: Array as PropType<T[]>,
    itemKey: String as PropType<keyof T>
  }
}))

@jods4
Copy link

jods4 commented Jun 20, 2021

@KaelWD I wrote my thoughts about why generics are needed to fully enable type-safe views in #334.
I'm interested in your similar experience, if you can comment?

@xiaoxiangmoe
Copy link

xiaoxiangmoe commented Dec 27, 2021

I want to defineComponent like this:

defineComponent<
  <T, T1, T2>() =>
    | {
        $props: {
          value: T
          foo: T1
          bar: T2
          titleRenderProp: (value?: T) => VNode 
        }
        $emit: {
          (event: 'update:value', value: T): void
        }
      }
    | {
        $props: {
          value: T
          foo: [T1]
          bar: [T1, T2]
        }
        $emit: {
          (event: 'update:value', value: T): void
        }
        $slots: {
          title: (slotProps: { value?: T }) => VNode[] | undefined
        }
      }
>({
  props: ['value'],
  setup() {}
})

Because the types of emits, slots, and props often affect each other.

@Kingwl
Copy link
Member

Kingwl commented Dec 27, 2021

It's very helpful to me!

@dakt
Copy link

dakt commented Apr 25, 2022

Will this ever be solved? Makes TS almost useless in large Vue3 projects, especially when one is writing a lot of reusable/library code. Generic components and typed slots are must IMO.

@sxzz
Copy link
Member

sxzz commented Oct 20, 2022

I just add the defineSlots feature for Vue Macros (unplugin-vue-macros@0.14.0-beta.2). Even though we have smart Volar helping us to detect slots in the template and generate type info, it's not enough I think.

So I implement this feature. With it, it's possible to declare slot type in <script setup>.

Basic Example

<script setup lang="ts">
defineSlots<{
  // slot name
  title: {
    // scoped slot
    foo: 'bar' | boolean
  }
}>()
</script>

Read the Documentation

@microHoffman
Copy link

It would be so great to have this finalized!

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

Successfully merging this pull request may close these issues.

None yet