diff --git a/docs/rules/README.md b/docs/rules/README.md
index 4343ba647..97f4b44b4 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -247,6 +247,7 @@ For example:
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | :hammer: |
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | :hammer: |
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | :hammer: |
+| [vue/optional-props-using-with-defaults](./optional-props-using-with-defaults.md) | enforce props with default values to be optional | :wrench: | :hammer: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
diff --git a/docs/rules/optional-props-using-with-defaults.md b/docs/rules/optional-props-using-with-defaults.md
new file mode 100644
index 000000000..770400d06
--- /dev/null
+++ b/docs/rules/optional-props-using-with-defaults.md
@@ -0,0 +1,55 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/optional-props-using-with-defaults
+description: enforce props with default values to be optional
+---
+# vue/optional-props-using-with-defaults
+
+> enforce props with default values to be optional
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule enforce props with default values to be optional.
+Because when a required prop declared with a default value, but it doesn't be passed value when using it, it will be assigned the default value. So a required prop with default value is same as a optional prop.
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/optional-props-using-with-defaults.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/optional-props-using-with-defaults.js)
diff --git a/lib/index.js b/lib/index.js
index c4979a98c..540dd5212 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -154,6 +154,7 @@ module.exports = {
'object-shorthand': require('./rules/object-shorthand'),
'one-component-per-file': require('./rules/one-component-per-file'),
'operator-linebreak': require('./rules/operator-linebreak'),
+ 'optional-props-using-with-defaults': require('./rules/optional-props-using-with-defaults'),
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
diff --git a/lib/rules/optional-props-using-with-defaults.js b/lib/rules/optional-props-using-with-defaults.js
new file mode 100644
index 000000000..2593715c5
--- /dev/null
+++ b/lib/rules/optional-props-using-with-defaults.js
@@ -0,0 +1,96 @@
+/**
+ * @author @neferqiqi
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+/**
+ * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
+ */
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+// ...
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce props with default values to be optional',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/optional-props-using-with-defaults.html'
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ // ...
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /**
+ * @param {ComponentTypeProp} prop
+ * @param {Token[]} tokens
+ * */
+ const findKeyToken = (prop, tokens) =>
+ tokens.find((token) => {
+ const isKeyIdentifierEqual =
+ prop.key.type === 'Identifier' && token.value === prop.key.name
+ const isKeyLiteralEqual =
+ prop.key.type === 'Literal' && token.value === prop.key.raw
+ return isKeyIdentifierEqual || isKeyLiteralEqual
+ })
+
+ return utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node, props) {
+ if (!utils.hasWithDefaults(node)) {
+ return
+ }
+ const withDefaultsProps = Object.keys(
+ utils.getWithDefaultsPropExpressions(node)
+ )
+ const requiredProps = props.flatMap((item) =>
+ item.type === 'type' && item.required ? [item] : []
+ )
+
+ for (const prop of requiredProps) {
+ if (withDefaultsProps.includes(prop.propName)) {
+ const firstToken = context.getSourceCode().getFirstToken(prop.node)
+ // skip setter & getter case
+ if (firstToken.value === 'get' || firstToken.value === 'set') {
+ return
+ }
+ // skip computed
+ if (prop.node.computed) {
+ return
+ }
+ const keyToken = findKeyToken(
+ prop,
+ context.getSourceCode().getTokens(prop.node)
+ )
+ if (!keyToken) return
+ context.report({
+ node: prop.node,
+ loc: prop.node.loc,
+ data: {
+ key: prop.propName
+ },
+ message: `Prop "{{ key }}" should be optional.`,
+ fix: (fixer) => fixer.insertTextAfter(keyToken, '?')
+ })
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/optional-props-using-with-defaults.js b/tests/lib/rules/optional-props-using-with-defaults.js
new file mode 100644
index 000000000..a4f7d212c
--- /dev/null
+++ b/tests/lib/rules/optional-props-using-with-defaults.js
@@ -0,0 +1,665 @@
+/**
+ * @author neferqiqi
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/optional-props-using-with-defaults')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('optional-props-using-with-defaults', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "na::me" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "na"me2" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "foo" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "foo" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "name" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "a" should be optional.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: 'Prop "a" should be optional.',
+ line: 4
+ }
+ ]
+ }
+ ]
+})