diff --git a/docs/rules/require-emit-validator.md b/docs/rules/require-emit-validator.md
new file mode 100644
index 000000000..9ca6df169
--- /dev/null
+++ b/docs/rules/require-emit-validator.md
@@ -0,0 +1,61 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-emit-validator
+description: require type definitions in emits
+---
+# vue/require-emit-validator
+
+> require type definitions in emits
+
+- :exclamation: ***This rule has not been released yet.***
+- :gear: This rule is included in .
+
+## :book: Rule Details
+
+This rule enforces that a `emits` statement contains type definition.
+
+Declaring `emits` with types can bring better maintenance.
+Even if using with TypeScript, this can provide better type inference when annotating parameters with types.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [API Reference](https://v3.vuejs.org/api/options-data.html#emits)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-emit-validator.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-emit-validator.js)
diff --git a/lib/index.js b/lib/index.js
index d38cff322..1379e284e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -140,6 +140,7 @@ module.exports = {
'require-component-is': require('./rules/require-component-is'),
'require-default-prop': require('./rules/require-default-prop'),
'require-direct-export': require('./rules/require-direct-export'),
+ 'require-emit-validator': require('./rules/require-emit-validator'),
'require-explicit-emits': require('./rules/require-explicit-emits'),
'require-name-property': require('./rules/require-name-property'),
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
diff --git a/lib/rules/require-emit-validator.js b/lib/rules/require-emit-validator.js
new file mode 100644
index 000000000..c85ee42ad
--- /dev/null
+++ b/lib/rules/require-emit-validator.js
@@ -0,0 +1,90 @@
+/**
+ * @fileoverview Emit definitions should be detailed
+ * @author Pig Fang
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit
+ * @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit
+ */
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'require type definitions in emits',
+ categories: [],
+ url: 'https://eslint.vuejs.org/rules/require-emit-validator.html'
+ },
+ fixable: null,
+ messages: {
+ missing: 'Emit "{{name}}" should define at least its validator function.',
+ skipped:
+ 'Emit "{{name}}" should not skip validation, or you may define a validator function with no parameters.',
+ emptyValidation: 'Replace with a validator function with no parameters.'
+ },
+ schema: []
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ // ----------------------------------------------------------------------
+ // Helpers
+ // ----------------------------------------------------------------------
+
+ /**
+ * @param {ComponentArrayEmit|ComponentObjectEmit} emit
+ */
+ function checker({ value, node, emitName }) {
+ const hasType =
+ !!value &&
+ (value.type === 'ArrowFunctionExpression' ||
+ value.type === 'FunctionExpression' ||
+ // validator may from outer scope
+ value.type === 'Identifier')
+
+ if (!hasType) {
+ const name =
+ emitName ||
+ (node.type === 'Identifier' && node.name) ||
+ 'Unknown emit'
+
+ if (value && value.type === 'Literal' && value.value === null) {
+ context.report({
+ node,
+ messageId: 'skipped',
+ data: { name },
+ suggest: [
+ {
+ messageId: 'emptyValidation',
+ fix: (fixer) => fixer.replaceText(value, '() => true')
+ }
+ ]
+ })
+
+ return
+ }
+
+ context.report({
+ node,
+ messageId: 'missing',
+ data: { name }
+ })
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // Public
+ // ----------------------------------------------------------------------
+
+ return utils.executeOnVue(context, (obj) => {
+ utils.getComponentEmits(obj).forEach(checker)
+ })
+ }
+}
diff --git a/tests/lib/rules/require-emit-validator.js b/tests/lib/rules/require-emit-validator.js
new file mode 100644
index 000000000..6c1170e95
--- /dev/null
+++ b/tests/lib/rules/require-emit-validator.js
@@ -0,0 +1,330 @@
+/**
+ * @fileoverview Emit definitions should be detailed
+ * @author Pig Fang
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/require-emit-validator')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('require-emit-validator', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ ...foo,
+ emits: {
+ ...test(),
+ foo: (payload) => typeof payload === 'object'
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo: (payload) => typeof payload === 'object'
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo(payload) {
+ return typeof payload === 'object'
+ }
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo: () => {}
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo() {}
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: externalEmits
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: []
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {}
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default defineComponent({
+ emits: {
+ foo: (payload: string | number) => true,
+ }
+ })
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default defineComponent({
+ emits: {
+ foo(payload: string | number) {
+ return true
+ },
+ },
+ })
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ function foo () {}
+ export default {
+ emits: {
+ foo
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { isNumber } from './mod'
+ export default {
+ emits: {
+ foo: isNumber
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+ }
+ ],
+
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: ['foo', bar, \`baz\`, foo()]
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ errors: [
+ {
+ messageId: 'missing',
+ data: { name: 'foo' },
+ line: 3
+ },
+ {
+ messageId: 'missing',
+ data: { name: 'bar' },
+ line: 3
+ },
+ {
+ messageId: 'missing',
+ data: { name: 'baz' },
+ line: 3
+ },
+ {
+ messageId: 'missing',
+ data: { name: 'Unknown emit' },
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.js',
+ code: `
+ new Vue({
+ emits: ['foo', bar, \`baz\`, foo()]
+ })
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ errors: [
+ {
+ messageId: 'missing',
+ data: { name: 'foo' },
+ line: 3
+ },
+ {
+ messageId: 'missing',
+ data: { name: 'bar' },
+ line: 3
+ },
+ {
+ messageId: 'missing',
+ data: { name: 'baz' },
+ line: 3
+ },
+ {
+ messageId: 'missing',
+ data: { name: 'Unknown emit' },
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo: null
+ }
+ }`,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ errors: [
+ {
+ messageId: 'skipped',
+ data: { name: 'foo' },
+ line: 4,
+ suggestions: [
+ {
+ messageId: 'emptyValidation',
+ output: `
+ export default {
+ emits: {
+ foo: () => true
+ }
+ }`
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo: null,
+ bar: (payload) => {}
+ }
+ }`,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ errors: [
+ {
+ messageId: 'skipped',
+ data: { name: 'foo' },
+ line: 4,
+ suggestions: [
+ {
+ messageId: 'emptyValidation',
+ output: `
+ export default {
+ emits: {
+ foo: () => true,
+ bar: (payload) => {}
+ }
+ }`
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default {
+ emits: {
+ foo: {
+ type: String
+ }
+ }
+ }
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ errors: [
+ {
+ messageId: 'missing',
+ data: { name: 'foo' },
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ export default defineComponent({
+ emits: {
+ foo: {} as ((payload: string) => boolean)
+ }
+ });
+ `,
+ parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+ parser: require.resolve('@typescript-eslint/parser'),
+ errors: [
+ {
+ messageId: 'missing',
+ data: { name: 'foo' },
+ line: 4
+ }
+ ]
+ }
+ ]
+})