Repository: vuejs/eslint-plugin-vue (js)

Issue:
`require-explicit-slots` doesn't recognize interface passed to the macro
**Tell us about your environment**

- **ESLint version:** `8.56.0`
- **eslint-plugin-vue version:** `9.21.1`
- **Vue version:** `3.4.15`
- **Node version:** `20.10.0`
- **Operating System:** `Mac OS 14.2.1`

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5
module.exports = {
  parser: 'vue-eslint-parser',

  parserOptions: {
    parser: '@typescript-eslint/parser',
  },

  extends: ['plugin:vue/base'],

  rules: {
    'vue/require-explicit-slots': 'error',
  },
};
```

**What did you do?**	
<!-- Please include the actual source code causing the issue. -->
```vue
<template>
  <div>
    <slot />
  </div>
</template>

<script lang="ts" setup>
interface Slots {
  default: () => string;
}

defineSlots<Slots>();
</script>

```

**What did you expect to happen?**
No error reported.

**What actually happened?**
```
test.vue
  3:5  error  Slots must be explicitly defined  vue/require-explicit-slots

✖ 1 problem (1 error, 0 warnings)
```

**Repository to reproduce this issue**
https://stackblitz.com/edit/stackblitz-starters-reg1dy?file=test.vue

```
~$ npm run lint
```



Pull request:
fix(require-explicit-slots): add support for type references
This pull request resolves #2398 using an approach requested within #2484.

Patch:
diff --git a/lib/rules/require-explicit-slots.js b/lib/rules/require-explicit-slots.js
index f87503bb..5298e598 100644
--- a/lib/rules/require-explicit-slots.js
+++ b/lib/rules/require-explicit-slots.js
@@ -98,30 +98,22 @@ module.exports = {
 
     return utils.compositingVisitors(
       utils.defineScriptSetupVisitor(context, {
-        onDefineSlotsEnter(node) {
-          const typeArguments =
-            'typeArguments' in node ? node.typeArguments : node.typeParameters
-          const param = /** @type {TypeNode|undefined} */ (
-            typeArguments?.params[0]
-          )
-          if (!param) return
-
-          if (param.type === 'TSTypeLiteral') {
-            for (const memberNode of param.members) {
-              const slotName = getSlotsName(memberNode)
-              if (!slotName) continue
-
-              if (slotsDefined.has(slotName)) {
-                context.report({
-                  node: memberNode,
-                  messageId: 'alreadyDefinedSlot',
-                  data: {
-                    slotName
-                  }
-                })
-              } else {
-                slotsDefined.add(slotName)
-              }
+        onDefineSlotsEnter(_node, slots) {
+          for (const slot of slots) {
+            if (!slot.slotName) {
+              continue
+            }
+
+            if (slotsDefined.has(slot.slotName)) {
+              context.report({
+                node: slot.node,
+                messageId: 'alreadyDefinedSlot',
+                data: {
+                  slotName: slot.slotName
+                }
+              })
+            } else {
+              slotsDefined.add(slot.slotName)
             }
           }
         }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index eb84c127..167edf20 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -26,6 +26,10 @@ const { getScope } = require('./scope')
  * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit
  * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit
  * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentEmit} ComponentEmit
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeSlot} ComponentTypeSlot
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeSlot} ComponentInferTypeSlot
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownSlot} ComponentUnknownSlot
+ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentSlot} ComponentSlot
  * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModelName} ComponentModelName
  * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModel} ComponentModel
  */
@@ -70,6 +74,7 @@ const {
 const {
   getComponentPropsFromTypeDefine,
   getComponentEmitsFromTypeDefine,
+  getComponentSlotsFromTypeDefine,
   isTypeNode
 } = require('./ts-utils')
 
@@ -1435,7 +1440,7 @@ module.exports = {
         'onDefineSlotsEnter',
         'onDefineSlotsExit',
         (candidateMacro, node) => candidateMacro === node,
-        () => undefined
+        getComponentSlotsFromDefineSlots
       ),
       new MacroListener(
         'defineExpose',
@@ -3372,6 +3377,28 @@ function getComponentEmitsFromDefineEmits(context, node) {
     }
   ]
 }
+
+/**
+ * Get all slots from `defineSlots` call expression.
+ * @param {RuleContext} context The rule context object.
+ * @param {CallExpression} node `defineSlots` call expression
+ * @return {ComponentSlot[]} Array of component slots
+ */
+function getComponentSlotsFromDefineSlots(context, node) {
+  const typeArguments =
+    'typeArguments' in node ? node.typeArguments : node.typeParameters
+  if (typeArguments && typeArguments.params.length > 0) {
+    return getComponentSlotsFromTypeDefine(context, typeArguments.params[0])
+  }
+  return [
+    {
+      type: 'unknown',
+      slotName: null,
+      node: null
+    }
+  ]
+}
+
 /**
  * Get model info from `defineModel` call expression.
  * @param {RuleContext} _context The rule context object.
@@ -3414,6 +3441,7 @@ function getComponentModelFromDefineModel(_context, node) {
     typeNode: null
   }
 }
+
 /**
  * Get all props by looking at all component's properties
  * @param {ObjectExpression|ArrayExpression} propsNode Object with props definition
diff --git a/lib/utils/ts-utils/index.js b/lib/utils/ts-utils/index.js
index 8b6c53b2..3db610d1 100644
--- a/lib/utils/ts-utils/index.js
+++ b/lib/utils/ts-utils/index.js
@@ -5,11 +5,13 @@ const {
   isTSTypeLiteralOrTSFunctionType,
   extractRuntimeEmits,
   flattenTypeNodes,
-  isTSInterfaceBody
+  isTSInterfaceBody,
+  extractRuntimeSlots
 } = require('./ts-ast')
 const {
   getComponentPropsFromTypeDefineTypes,
-  getComponentEmitsFromTypeDefineTypes
+  getComponentEmitsFromTypeDefineTypes,
+  getComponentSlotsFromTypeDefineTypes
 } = require('./ts-types')
 
 /**
@@ -22,12 +24,16 @@ const {
  * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
  * @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
  * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
+ * @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
+ * @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
+ * @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
  */
 
 module.exports = {
   isTypeNode,
   getComponentPropsFromTypeDefine,
-  getComponentEmitsFromTypeDefine
+  getComponentEmitsFromTypeDefine,
+  getComponentSlotsFromTypeDefine
 }
 
 /**
@@ -86,3 +92,30 @@ function getComponentEmitsFromTypeDefine(context, emitsNode) {
   }
   return result
 }
+
+/**
+ * Get all slots by looking at all component's properties
+ * @param {RuleContext} context The ESLint rule context object.
+ * @param {TypeNode} slotsNode Type with slots definition
+ * @return {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
+ */
+function getComponentSlotsFromTypeDefine(context, slotsNode) {
+  /** @type {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} */
+  const result = []
+  for (const defNode of flattenTypeNodes(
+    context,
+    /** @type {TSESTreeTypeNode} */ (slotsNode)
+  )) {
+    if (isTSInterfaceBody(defNode) || isTSTypeLiteral(defNode)) {
+      result.push(...extractRuntimeSlots(defNode))
+    } else {
+      result.push(
+        ...getComponentSlotsFromTypeDefineTypes(
+          context,
+          /** @type {TypeNode} */ (defNode)
+        )
+      )
+    }
+  }
+  return result
+}
diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js
index ddbb9de0..1021b4ba 100644
--- a/lib/utils/ts-utils/ts-ast.js
+++ b/lib/utils/ts-utils/ts-ast.js
@@ -15,6 +15,8 @@ const { inferRuntimeTypeFromTypeNode } = require('./ts-types')
  * @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
  * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
  * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
+ * @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
+ * @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
  */
 
 const noop = Function.prototype
@@ -26,7 +28,8 @@ module.exports = {
   isTSTypeLiteral,
   isTSTypeLiteralOrTSFunctionType,
   extractRuntimeProps,
-  extractRuntimeEmits
+  extractRuntimeEmits,
+  extractRuntimeSlots
 }
 
 /**
@@ -209,6 +212,38 @@ function* extractRuntimeEmits(node) {
   }
 }
 
+/**
+ * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node
+ * @returns {IterableIterator<ComponentTypeSlot | ComponentUnknownSlot>}
+ */
+function* extractRuntimeSlots(node) {
+  const members = node.type === 'TSTypeLiteral' ? node.members : node.body
+  for (const member of members) {
+    if (
+      member.type === 'TSPropertySignature' ||
+      member.type === 'TSMethodSignature'
+    ) {
+      if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') {
+        yield {
+          type: 'unknown',
+          slotName: null,
+          node: /** @type {Expression} */ (member.key)
+        }
+        continue
+      }
+      yield {
+        type: 'type',
+        key: /** @type {Identifier | Literal} */ (member.key),
+        slotName:
+          member.key.type === 'Identifier'
+            ? member.key.name
+            : `${member.key.value}`,
+        node: /** @type {TSPropertySignature | TSMethodSignature} */ (member)
+      }
+    }
+  }
+}
+
 /**
  * @param {TSESTreeParameter} eventName
  * @param {TSCallSignatureDeclaration | TSFunctionType} member
diff --git a/lib/utils/ts-utils/ts-types.js b/lib/utils/ts-utils/ts-types.js
index abb30386..2fe354c2 100644
--- a/lib/utils/ts-utils/ts-types.js
+++ b/lib/utils/ts-utils/ts-types.js
@@ -24,11 +24,14 @@ const {
  * @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
  * @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
  * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
+ * @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
+ * @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
  */
 
 module.exports = {
   getComponentPropsFromTypeDefineTypes,
   getComponentEmitsFromTypeDefineTypes,
+  getComponentSlotsFromTypeDefineTypes,
   inferRuntimeTypeFromTypeNode
 }
 
@@ -122,6 +125,34 @@ function getComponentEmitsFromTypeDefineTypes(context, emitsNode) {
   return [...extractRuntimeEmits(type, tsNode, emitsNode, services)]
 }
 
+/**
+ * Get all slots by looking at all component's properties
+ * @param {RuleContext} context The ESLint rule context object.
+ * @param {TypeNode} slotsNode Type with slots definition
+ * @return {(ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
+ */
+function getComponentSlotsFromTypeDefineTypes(context, slotsNode) {
+  const services = getTSParserServices(context)
+  const tsNode = services && services.tsNodeMap.get(slotsNode)
+  const type = tsNode && services.checker.getTypeAtLocation(tsNode)
+  if (
+    !type ||
+    isAny(type) ||
+    isUnknown(type) ||
+    isNever(type) ||
+    isNull(type)
+  ) {
+    return [
+      {
+        type: 'unknown',
+        slotName: null,
+        node: slotsNode
+      }
+    ]
+  }
+  return [...extractRuntimeSlots(type, slotsNode)]
+}
+
 /**
  * @param {RuleContext} context The ESLint rule context object.
  * @param {TypeNode|Expression} node
@@ -259,6 +290,23 @@ function* extractRuntimeEmits(type, tsNode, emitsNode, services) {
   }
 }
 
+/**
+ * @param {Type} type
+ * @param {TypeNode} slotsNode Type with slots definition
+ * @returns {IterableIterator<ComponentInferTypeSlot>}
+ */
+function* extractRuntimeSlots(type, slotsNode) {
+  for (const property of type.getProperties()) {
+    const name = property.getName()
+
+    yield {
+      type: 'infer-type',
+      slotName: name,
+      node: slotsNode
+    }
+  }
+}
+
 /**
  * @param {Type} type
  * @returns {Iterable<Type>}
diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts
index 3e918426..ebe9933d 100644
--- a/typings/eslint-plugin-vue/util-types/utils.ts
+++ b/typings/eslint-plugin-vue/util-types/utils.ts
@@ -42,8 +42,8 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
   onDefineEmitsExit?(node: CallExpression, emits: ComponentEmit[]): void
   onDefineOptionsEnter?(node: CallExpression): void
   onDefineOptionsExit?(node: CallExpression): void
-  onDefineSlotsEnter?(node: CallExpression): void
-  onDefineSlotsExit?(node: CallExpression): void
+  onDefineSlotsEnter?(node: CallExpression, slots: ComponentSlot[]): void
+  onDefineSlotsExit?(node: CallExpression, slots: ComponentSlot[]): void
   onDefineExposeEnter?(node: CallExpression): void
   onDefineExposeExit?(node: CallExpression): void
   onDefineModelEnter?(node: CallExpression, model: ComponentModel): void
@@ -52,6 +52,7 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
     | ((node: VAST.ParamNode) => void)
     | ((node: CallExpression, props: ComponentProp[]) => void)
     | ((node: CallExpression, emits: ComponentEmit[]) => void)
+    | ((node: CallExpression, slots: ComponentSlot[]) => void)
     | ((node: CallExpression, model: ComponentModel) => void)
     | undefined
 }
@@ -191,6 +192,30 @@ export type ComponentEmit =
   | ComponentInferTypeEmit
   | ComponentUnknownEmit
 
+export type ComponentUnknownSlot = {
+  type: 'unknown'
+  slotName: null
+  node: Expression | SpreadElement | TypeNode | null
+}
+
+export type ComponentTypeSlot = {
+  type: 'type'
+  key: Identifier | Literal
+  slotName: string
+  node: TSPropertySignature | TSMethodSignature
+}
+
+export type ComponentInferTypeSlot = {
+  type: 'infer-type'
+  slotName: string
+  node: TypeNode
+}
+
+export type ComponentSlot =
+  | ComponentTypeSlot
+  | ComponentInferTypeSlot
+  | ComponentUnknownSlot
+
 export type ComponentModelName = {
   modelName: string
   node: Literal | null


Test patch:
diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js
index 92d1a133..f9961411 100644
--- a/tests/lib/rules/require-explicit-slots.js
+++ b/tests/lib/rules/require-explicit-slots.js
@@ -34,6 +34,36 @@ tester.run('require-explicit-slots', rule, {
       }>()
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot />
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot />
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
     {
       filename: 'test.vue',
       code: `
@@ -48,6 +78,36 @@ tester.run('require-explicit-slots', rule, {
       }>()
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
     {
       filename: 'test.vue',
       code: `
@@ -62,6 +122,36 @@ tester.run('require-explicit-slots', rule, {
       }>()
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        foo(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        foo(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
     {
       filename: 'test.vue',
       code: `
@@ -76,6 +166,36 @@ tester.run('require-explicit-slots', rule, {
       }>()
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        foo: (props: { msg: string }) => any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        foo: (props: { msg: string }) => any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
     {
       filename: 'test.vue',
       code: `
@@ -90,6 +210,36 @@ tester.run('require-explicit-slots', rule, {
       }>()
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo-bar"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        'foo-bar'(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo-bar"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        'foo-bar'(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
     {
       filename: 'test.vue',
       code: `
@@ -178,6 +328,40 @@ tester.run('require-explicit-slots', rule, {
       }>()
       </script>`
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="'foo'"></slot>
+          <slot :name="\`bar\`"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        foo(props: { msg: string }): any
+        bar(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="'foo'"></slot>
+          <slot :name="\`bar\`"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        foo(props: { msg: string }): any
+        bar(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
     {
       filename: 'test.vue',
       code: `
@@ -191,6 +375,36 @@ tester.run('require-explicit-slots', rule, {
         default(props: { msg: string }): any
       }>()
       </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="bar"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="bar"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`
     }
   ],
   invalid: [
@@ -261,6 +475,46 @@ tester.run('require-explicit-slots', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -280,6 +534,46 @@ tester.run('require-explicit-slots', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        default: (props: { msg: string }) => any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        default: (props: { msg: string }) => any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -299,6 +593,46 @@ tester.run('require-explicit-slots', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name />
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        'foo-bar'(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name />
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        'foo-bar'(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -342,6 +676,48 @@ tester.run('require-explicit-slots', rule, {
         }
       ]
     },
+    {
+      // ignore attribute binding except string literal
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="'foo'"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
+    {
+      // ignore attribute binding except string literal
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot :name="'foo'"></slot>
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        default(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slots must be explicitly defined.'
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -362,6 +738,48 @@ tester.run('require-explicit-slots', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface Slots {
+        foo(props: { msg: string }): any
+        foo(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slot foo is already defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      type Slots = {
+        foo(props: { msg: string }): any
+        foo(props: { msg: string }): any
+      }
+      defineSlots<Slots>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slot foo is already defined.'
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -384,6 +802,56 @@ tester.run('require-explicit-slots', rule, {
           message: 'Slot foo is already defined.'
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      interface SlotsA {
+        foo(props: { msg: string }): any
+      }
+      defineSlots<SlotsA>()
+      interface SlotsB {
+        default(props: { msg: string }): any,
+        foo(props: { msg: string }): any
+      }
+      defineSlots<SlotsB>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slot foo is already defined.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <slot name="foo" />
+        </div>
+      </template>
+      <script setup lang="ts">
+      type SlotsA = {
+        foo(props: { msg: string }): any
+      }
+      defineSlots<SlotsA>()
+      type SlotsB = {
+        default(props: { msg: string }): any,
+        foo(props: { msg: string }): any
+      }
+      defineSlots<SlotsB>()
+      </script>`,
+      errors: [
+        {
+          message: 'Slot foo is already defined.'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/utils/ts-utils/index/get-component-slots.js b/tests/lib/utils/ts-utils/index/get-component-slots.js
new file mode 100644
index 00000000..410021b9
--- /dev/null
+++ b/tests/lib/utils/ts-utils/index/get-component-slots.js
@@ -0,0 +1,115 @@
+/**
+ * Test for getComponentSlotsFromTypeDefineTypes
+ */
+'use strict'
+
+const path = require('path')
+const fs = require('fs')
+const Linter = require('../../../../eslint-compat').Linter
+const parser = require('vue-eslint-parser')
+const tsParser = require('@typescript-eslint/parser')
+const utils = require('../../../../../lib/utils/index')
+const assert = require('assert')
+
+const FIXTURES_ROOT = path.resolve(
+  __dirname,
+  '../../../../fixtures/utils/ts-utils'
+)
+const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json')
+const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts')
+
+function extractComponentSlots(code, tsFileCode) {
+  const linter = new Linter()
+  const result = []
+  const config = {
+    files: ['**/*.vue'],
+    languageOptions: {
+      parser,
+      ecmaVersion: 2020,
+      parserOptions: {
+        parser: tsParser,
+        project: [TSCONFIG_PATH],
+        extraFileExtensions: ['.vue']
+      }
+    },
+    plugins: {
+      test: {
+        rules: {
+          test: {
+            create(context) {
+              return utils.defineScriptSetupVisitor(context, {
+                onDefineSlotsEnter(_node, slots) {
+                  result.push(
+                    ...slots.map((prop) => ({
+                      type: prop.type,
+                      name: prop.slotName
+                    }))
+                  )
+                }
+              })
+            }
+          }
+        }
+      }
+    },
+    rules: {
+      'test/test': 'error'
+    }
+  }
+  fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8')
+  // clean './src/test.ts' cache
+  tsParser.clearCaches()
+  assert.deepStrictEqual(
+    linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')),
+    []
+  )
+  // reset
+  fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8')
+  return result
+}
+
+describe('getComponentSlotsFromTypeDefineTypes', () => {
+  for (const { scriptCode, tsFileCode, slots: expected } of [
+    {
+      scriptCode: `
+        defineSlots<{
+          default(props: { msg: string }): any
+        }>()
+      `,
+      slots: [{ type: 'type', name: 'default' }]
+    },
+    {
+      scriptCode: `
+        interface Slots {
+          default(props: { msg: string }): any
+        }
+        defineSlots<Slots>()
+      `,
+      slots: [{ type: 'type', name: 'default' }]
+    },
+    {
+      scriptCode: `
+        type Slots = {
+          default(props: { msg: string }): any
+        }
+        defineSlots<Slots>()
+      `,
+      slots: [{ type: 'type', name: 'default' }]
+    }
+  ]) {
+    const code = `
+      <script setup lang="ts">
+      ${scriptCode}
+      </script>
+    `
+    it(`should return expected slots with :${code}`, () => {
+      const slots = extractComponentSlots(code, tsFileCode)
+
+      assert.deepStrictEqual(
+        slots,
+        expected,
+        `\n${JSON.stringify(slots)}\n === \n${JSON.stringify(expected)}`
+      )
+    })
+  }
+})