diff --git a/docs/rules/index.md b/docs/rules/index.md
index c3e4b8ed7..9b7d52e76 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -264,6 +264,7 @@ For example:
| [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | :hammer: |
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | :bulb: | :hammer: |
| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment | | :hammer: |
+| [vue/require-typed-ref](./require-typed-ref.md) | require `ref` and `shallowRef` functions to be strongly typed | | :hammer: |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-typed-ref.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-typed-ref.js)
diff --git a/lib/index.js b/lib/index.js
index 8ac5d9383..42a223de6 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -188,6 +188,7 @@ module.exports = {
'require-render-return': require('./rules/require-render-return'),
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
+ 'require-typed-ref': require('./rules/require-typed-ref'),
'require-v-for-key': require('./rules/require-v-for-key'),
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
'return-in-computed-property': require('./rules/return-in-computed-property'),
diff --git a/lib/rules/require-typed-ref.js b/lib/rules/require-typed-ref.js
new file mode 100644
index 000000000..f8fce39de
--- /dev/null
+++ b/lib/rules/require-typed-ref.js
@@ -0,0 +1,102 @@
+/**
+ * @author Ivan Demchuk
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { iterateDefineRefs } = require('../utils/ref-object-references')
+const utils = require('../utils')
+
+/**
+ * @param {Expression|SpreadElement} node
+ */
+function isNullOrUndefined(node) {
+ return (
+ (node.type === 'Literal' && node.value === null) ||
+ (node.type === 'Identifier' && node.name === 'undefined')
+ )
+}
+
+/**
+ * @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
+ */
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'require `ref` and `shallowRef` functions to be strongly typed',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/require-typed-ref.html'
+ },
+ fixable: null,
+ messages: {
+ noType:
+ 'Specify type parameter for `{{name}}` function, otherwise created variable will not by typechecked.'
+ },
+ schema: []
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const filename = context.getFilename()
+ if (!utils.isVueFile(filename) && !utils.isTypeScriptFile(filename)) {
+ return {}
+ }
+
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (
+ scriptSetup &&
+ !utils.hasAttribute(scriptSetup, 'lang', 'ts') &&
+ !utils.hasAttribute(scriptSetup, 'lang', 'typescript')
+ ) {
+ return {}
+ }
+
+ const defines = iterateDefineRefs(context.getScope())
+
+ /**
+ * @param {string} name
+ * @param {CallExpression} node
+ */
+ function report(name, node) {
+ context.report({
+ node,
+ messageId: 'noType',
+ data: {
+ name
+ }
+ })
+ }
+
+ return {
+ Program() {
+ for (const ref of defines) {
+ if (ref.name !== 'ref' && ref.name !== 'shallowRef') {
+ continue
+ }
+
+ if (
+ ref.node.arguments.length > 0 &&
+ !isNullOrUndefined(ref.node.arguments[0])
+ ) {
+ continue
+ }
+
+ if (ref.node.typeParameters == null) {
+ if (
+ ref.node.parent.type === 'VariableDeclarator' &&
+ ref.node.parent.id.type === 'Identifier'
+ ) {
+ if (ref.node.parent.id.typeAnnotation == null) {
+ report(ref.name, ref.node)
+ }
+ } else {
+ report(ref.name, ref.node)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index ef8cd0ee9..1c54ef3f0 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -998,6 +998,8 @@ module.exports = {
return null
},
+ isTypeScriptFile,
+
isVueFile,
/**
@@ -2416,6 +2418,13 @@ function getVExpressionContainer(node) {
return n
}
+/**
+ * @param {string} path
+ */
+function isTypeScriptFile(path) {
+ return path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.mts')
+}
+
// ------------------------------------------------------------------------------
// Vue Helpers
// ------------------------------------------------------------------------------
diff --git a/lib/utils/ref-object-references.js b/lib/utils/ref-object-references.js
index ceecd89e3..49404dad0 100644
--- a/lib/utils/ref-object-references.js
+++ b/lib/utils/ref-object-references.js
@@ -251,6 +251,7 @@ function getGlobalScope(context) {
}
module.exports = {
+ iterateDefineRefs,
extractRefObjectReferences,
extractReactiveVariableReferences
}
diff --git a/tests/lib/rules/require-typed-ref.js b/tests/lib/rules/require-typed-ref.js
new file mode 100644
index 000000000..571b5c5de
--- /dev/null
+++ b/tests/lib/rules/require-typed-ref.js
@@ -0,0 +1,239 @@
+/**
+ * @author Ivan Demchuk
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/require-typed-ref')
+
+const tester = new RuleTester({
+ parser: require.resolve('@typescript-eslint/parser'),
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+// Note: Need to specify filename for each test,
+// as only TypeScript files are being checked
+tester.run('require-typed-ref', rule, {
+ valid: [
+ {
+ filename: 'test.ts',
+ code: `
+ import { shallowRef } from 'vue'
+ const count = shallowRef(0)
+ `
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const count = ref()
+ `
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const count = ref(0)
+ `
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const counter: Ref = ref()
+ `
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const count = ref(0)
+ `
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ function useCount() {
+ return {
+ count: ref()
+ }
+ }
+ `
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref, defineComponent } from 'vue'
+ defineComponent({
+ setup() {
+ const count = ref()
+ return { count }
+ }
+ })
+ `
+ },
+ {
+ filename: 'test.vue',
+ parser: require.resolve('vue-eslint-parser'),
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.js',
+ code: `
+ import { ref } from 'vue'
+ const count = ref()
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const count = ref()
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 3,
+ column: 23,
+ endLine: 3,
+ endColumn: 28
+ }
+ ]
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const count = ref(null)
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 3,
+ column: 23,
+ endLine: 3,
+ endColumn: 32
+ }
+ ]
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ const count = ref(undefined)
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 3,
+ column: 23,
+ endLine: 3,
+ endColumn: 37
+ }
+ ]
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { shallowRef } from 'vue'
+ const count = shallowRef()
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 3,
+ column: 23,
+ endLine: 3,
+ endColumn: 35
+ }
+ ]
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ function useCount() {
+ const count = ref()
+ return { count }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 4,
+ column: 25,
+ endLine: 4,
+ endColumn: 30
+ }
+ ]
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref } from 'vue'
+ function useCount() {
+ return {
+ count: ref()
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 5,
+ column: 20,
+ endLine: 5,
+ endColumn: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ parser: require.resolve('vue-eslint-parser'),
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 4,
+ column: 25,
+ endLine: 4,
+ endColumn: 30
+ }
+ ]
+ },
+ {
+ filename: 'test.ts',
+ code: `
+ import { ref, defineComponent } from 'vue'
+ defineComponent({
+ setup() {
+ const count = ref()
+ return { count }
+ }
+ })
+ `,
+ errors: [
+ {
+ messageId: 'noType',
+ line: 5,
+ column: 27,
+ endLine: 5,
+ endColumn: 32
+ }
+ ]
+ }
+ ]
+})