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

feat: add defineSlots macro and slots option #7982

Merged
merged 14 commits into from
Apr 3, 2023
Merged

Conversation

sxzz
Copy link
Member

@sxzz sxzz commented Mar 29, 2023

Summary

This PR adds slots type checking for:

  • Non-SFC Composition API via the new slots option and SlotsType utility type.
  • SFC w/ <script setup lang="ts"> via the new defineSlots macro.
    • Volar will be updated to support defineSlots in SFCs
  • Volar will support checking slot props / presence when consuming components with declared slot types
    • Note: slot presence checks are only performed when vueCompilerOptions.strictTemplates is enabled in tsconfig.json.

Basic Usage

<script setup lang="ts">

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { foo: string; bar: number }): any // or VNode[]
}>()
</script>

defineSlots() only accepts a type parameter and no runtime arguments. The type parameter should be a type literal where the property key is the slot name, and the value is a slot function. The first argument of the function is the props the slot expects to receive, and its type will be used for slot props in the template. The returning value of defineSlots is the same slots object returned from useSlots.

Some current limitations:

  • Required slots checking is not yet implemented in volar / vue-tsc.
  • Slot function return type is currently ignored and can be any, but we may leverage it for slot content checking in the future.

Note: a previous version of this feature (as of this PR) supported a shorter syntax defineSlots<{ foo: { id: string }}>() - but the type conversion required in this signature breaks when used along with generic types (ref: vuejs/language-tools#2758), so it was removed in 1279b17. Now the function syntax is required and the only supported syntax.

Options API

import { SlotsType } from 'vue'

defineComponent({
  slots: Object as SlotsType<{
    default: { foo: string; bar: number }
    item: { data: number }
  }>,
  setup(props, { slots }) {
    expectType<undefined | ((scope: { foo: string; bar: number }) => any)>(
      slots.default
    )
    expectType<undefined | ((scope: { data: number }) => any)>(slots.item)
  }
})

This PR did not add any runtime code, but only included TypeScript types and SFC compiler features. Both defineSlots and the slots option have no runtime implications and serve purely as type hints for IDEs and vue-tsc.

Related

RFC: vuejs/rfcs#192 (not quite according to RFC)

@sxzz sxzz force-pushed the feat/define-slots branch 2 times, most recently from e7c7901 to 652ac28 Compare March 30, 2023 06:24
@sxzz sxzz changed the title feat: add defineSlots macro feat: add defineSlots macro and slots option Mar 30, 2023
@sxzz sxzz marked this pull request as ready for review March 30, 2023 08:19
@sxzz sxzz requested a review from pikax March 30, 2023 09:46
@pikax
Copy link
Member

pikax commented Mar 30, 2023

Not a fan of the tuple syntax, because it does not accurate describes the javascript behaviour:

<template>
   <a>
    <slot title="hello" something="sss"/>
  </a>
</template>

The slot will be called with an object

const __sfc__ = {}
import { renderSlot as _renderSlot, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("a", null, [
    _renderSlot(_ctx.$slots, "default", { 
      title: "hello",
      something: "sss"
    })
  ]))
}
__sfc__.render = render
__sfc__.__file = "Comp.vue"
export default __sfc__

You can see this.$slots.default({ /* object */} (sugar), there's no need to change the declaration of it to a odd tuple.

expectType<undefined | ((foo: string, bar: number) => any)>(slots.default)

That's not the correct type, the correct type is:

expectType<((param: { foo: string, bar: number }) => VNode | VNode[])>(slots.default)

I would also argue that the slot cannot be undefined, to force the slot to be undefined it should be added ?, this would allow to have some control on a few components that might require slots to be passed in (this is not runtime spec).

See playground

@sxzz
Copy link
Member Author

sxzz commented Mar 30, 2023

@pikax Thanks for your review. I've changed the style of the definition.

But I think it should be

expectType<undefined | ((param: { foo: string; bar: number }) => VNode[])>(
  slots.default
)

because if the parent component didn't pass the slot, the slot is undefined. And if the parent component did, it's VNode[] (the same as Vue 3.2)

https://sfc.vuejs.org/#eNp9UbtuwzAM/BVBS1IglvbAKVD0E7pqcR3GdqAXRNodDP97STspghbpJt4dT9LdrN9yNtMI+qhrbMuQSSHQmF9dHEJOhdR7ClldSgpqZ6wMIt+5WNtNz0oeCEL2DQFPStWt7EwV+kQnpxunVxETVhgeavuzoA96u6kKTTZXTJHfMova3Qh0+qhWRDC+XWane6KMR2vx0sqTrmhS6SyfTBkjDQEMYKg+S/pCKGzs9OHBwzI4QakKxDMUKP95/pL+8RXbxcWFv3IP6Hme84jwwcHgckt1i7NNEVkrxOmu2L9sePJgfOr2K2vOcGlGT8w9rcBR3axN8EGWFA3kgavowfvktMIUgPohdowh8hfsVpyVtcdylm+i08CE

@pikax
Copy link
Member

pikax commented Mar 30, 2023

I don't fully agree just because it would allow people to force users to declare a slot.

using the same example

<template>
  <comp v-slot="a">
  </comp>
</template>

The typechecking should warn that the slot default is required as the user should be passing it. Like how required props work. But this is just my opinion not related with how the runtime actually works.

Also this get a bit more complicated as you can see here

@sxzz
Copy link
Member Author

sxzz commented Mar 30, 2023

@pikax I agree with you. We should have a better DX about slots, but I think we need more discussions about it and we likely need to add runtime code. So maybe in another PR?

@pikax
Copy link
Member

pikax commented Mar 30, 2023

I don't have a strong opinion on either be done on this or another PR, what I think is a bit odd the syntax be very similar to defineProps

const props = defineProps<{
  a?: string; // explicit optional
  b: string;
}>();

expectType<number | undefined>(prop.a)
expectType<number>(prop.b)

const slots = defineSlots<{
 a?:  { title: string }; 
 b: { title: string }; // must pass slot
}>()

expectType<undefined | (title: string)=> VNode[]>(slots.a)
expectType<(title: string)=> VNode[]>(slots.b)

Otherwise they just feel different

@jacekkarczmarczyk
Copy link
Contributor

Would it be possible to type-check missing slots or would it rather be a task for eslint or other tools?

@pikax
Copy link
Member

pikax commented Mar 30, 2023

Would it be possible to type-check missing slots or would it rather be a task for eslint or other tools?

It would be possible to do typechecking, altho not sure how Volar is currently doing (with JSX) but if used h, is possible to typecheck it to see if is passed.

/cc @johnsoncodehk

@jacekkarczmarczyk
Copy link
Contributor

One more question - what about dynamic slot names? For example in many table components you can have slots related to table columns, item.foo, item.bar etc. Would it be something like:

const slots = defineSlots<{
 [name in 'item.${string}']?:  { title: string };  // not even sure if the syntax is valid
}>()

@pikax
Copy link
Member

pikax commented Mar 30, 2023

One more question - what about dynamic slot names?

Yes

This would be possible:

interface MyAwesomeType {
    title: string;
    age: number;
}

const slots = defineSlots<{
    [name in `col.${keyof MyAwesomeType}`]?: { item: MyAwesomeType, index: number };
}>()

slots['col.age']
slots['col.title']
// @ts-expect-error not valid
slots['col.no']

playground

@johnsoncodehk
Copy link
Member

johnsoncodehk commented Mar 30, 2023

Would it be possible to type-check missing slots or would it rather be a task for eslint or other tools?

It's supported in volar / vue-tsc v1.3.8 when enabled vueCompilerOptions.strictTemplates.

image


I'm not sure if it's appropriate to simplify the slots type input, it removes the possibility to check the children type for TSX.

https://www.typescriptlang.org/docs/handbook/jsx.html#children-type-checking

Even typed $slots as is doesn't add much boilerplate.

const slots = defineSlots<{
-  default?: { foo: string; bar: number }
+  default?(props: { foo: string; bar: number }): any /* or VNode[] */
}>()

@sxzz
Copy link
Member Author

sxzz commented Mar 30, 2023

@pikax

const slots = defineSlots<{
 a?:  { title: string }; 
}>()

--- expectType<undefined | (title: string)=> VNode[]>(slots.a)
+++ expectType<undefined | (scope: { title: string }) => VNode[]>(slots.a)

There will be only one param for slot, so the first param should an object.

Also there are actually two cases if we added optional property. - Whether the slot is optional or the scope of the slot is optional.

const slots = defineSlots<{
 a?: { title: string }; 
}>()

expectType<undefined | (scope: { title: string }) => VNode[]>(slots.a) // case 1
expectType<(scope?: { title: string }) => VNode[]>(slots.a) // case 2

@pikax
Copy link
Member

pikax commented Mar 30, 2023

@sxzz the second case is not possible, the object keys can be optional, but the first argument will always be present(as soon as you don't call it manually, which is not advised if you don't know what you're doing)

@sxzz
Copy link
Member Author

sxzz commented Mar 30, 2023

@pikax I see and I updated the code. But there are lots of uses of call the slot directly without a scope param in many 3rd party libraries, even in unit tests of vue core.

Vue JSX Docs

I think there's a safer way to mark scope as optional

const slots = defineSlots<{
  default: { foo: string; bar: number }
  optionalSlot?: string // it's an optional slot, but the scope is always present
  optionalScope: undefined | string // it's a required slot, but the scope is optional (could be undefined)
}>()

@pikax
Copy link
Member

pikax commented Mar 31, 2023

I think there's a safer way to mark scope as optional

const slots = defineSlots<{
  default: { foo: string; bar: number }
  optionalSlot?: string // it's an optional slot, but the scope is always present
  optionalScope: undefined | string // it's a required slot, but the scope is optional (could be undefined)
}>()

I agree with that/

Seems when you use SFC it will always pass an object playground

@sxzz
Copy link
Member Author

sxzz commented Apr 1, 2023

/ecosystem-ci run

@vue-bot
Copy link
Contributor

vue-bot commented Apr 1, 2023

📝 Ran ecosystem CI: Open

suite result
naive-ui ❌ failure
nuxt ❌ failure
pinia ✅ success
router ✅ success
test-utils ❌ failure
vant ✅ success
vite-plugin-vue ✅ success
vitepress ✅ success
vue-macros ✅ success
vuetify ❌ failure
vueuse ✅ success

@yyx990803
Copy link
Member

Update: added support so that SlotsType and defineSlots also support using direct function types to declare slots. This may be needed if Volar supports strict children type checks in the future, or in cases where some component libraries internally use multiple arguments for slots.

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { foo: string; bar: number }): any // or VNode[]
}>()
</script>

@@ -1428,6 +1494,7 @@ declare const MyButton: DefineComponent<
ComponentOptionsMixin,
EmitsOptions,
string,
{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this part when I was reviewing - we cannot actually change the type arguments order of DefineComponent because it's a publicly-exported type and used in library type definitions.

@yyx990803
Copy link
Member

Just a note for the future: downstream libs like test-utils and vuetify rely on the argument order of public types like DefineComponent, ComponentPublicInstance and component option types. The order is adjusted in bdf557f.

@Pentadome
Copy link
Contributor

Is it possible to use generic types in SlotsType? Function Signature only gives an example for props.

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.

7 participants