'vue.js cannot use data property in computed method

I'm trying to use a property of my data in a computed method like this:

data() {
    return {
      ToDoItems: [
        { id: uniqueId("todo-"), label: "Learn Vue", done: false },
        {
          id: uniqueId("todo-"),
          label: "Create a Vue project with the CLI",
          done: true,
        },
        { id: uniqueId("todo-"), label: "Have fun", done: true },
        { id: uniqueId("todo-"), label: "Create a to-do list", done: false },
      ],
    };
  },
computed: {
listSummary() {
  const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
    .length;
  return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},

But the IDE (Visual Studio Code) and the compiler throw an error:

Property 'ToDoItems' does not exist on type 'ComponentPublicInstance<{}, {}, {}, {}, {}, EmitsOptions, {}, {}, false, ComponentOptionsBase<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string, {}>>'.

I'm following the vue.js tutorial of mozilla (https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_computed_properties#adding_a_summary_counter) but using v3.

Has anything changed that this isn't possible anymore / differently?

Thanks in advance

complete code:

<template>
  <div id="app">
    <h1>To-Do List</h1>
    <to-do-form @todo-added="addToDo"></to-do-form>
    <h2 id="list-summary">{{ listSummary }}</h2>
    <ul aria-labelledby="list-summary" class="stack-large">
      <li v-for="item in ToDoItems" :key="item.id">
        <to-do-item :label="item.label" :done="true" :id="item.id"></to-do-item>
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";

export default defineComponent({
  name: "App",
  components: {
    ToDoItem,
    ToDoForm,
  },
  data() {
    return {
      ToDoItems: [
        { id: uniqueId("todo-"), label: "Learn Vue", done: false },
        {
          id: uniqueId("todo-"),
          label: "Create a Vue project with the CLI",
          done: true,
        },
        { id: uniqueId("todo-"), label: "Have fun", done: true },
        { id: uniqueId("todo-"), label: "Create a to-do list", done: false },
      ],
    };
  },
  methods: {
    addToDo(toDoLabel: string) {
      this.ToDoItems.push({
        id: uniqueId("todo-"),
        label: toDoLabel,
        done: false,
      });
    },
  },
  computed: {
    listSummary() {
      const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
        .length;
      return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
    },
  },
});
</script>

<style>
/* Global styles */
.btn {
  padding: 0.8rem 1rem 0.7rem;
  border: 0.2rem solid #4d4d4d;
  cursor: pointer;
  text-transform: capitalize;
}
.btn__danger {
  color: #fff;
  background-color: #ca3c3c;
  border-color: #bd2130;
}
.btn__filter {
  border-color: lightgrey;
}
.btn__danger:focus {
  outline-color: #c82333;
}
.btn__primary {
  color: #fff;
  background-color: #000;
}
.btn-group {
  display: flex;
  justify-content: space-between;
}
.btn-group > * {
  flex: 1 1 auto;
}
.btn-group > * + * {
  margin-left: 0.8rem;
}
.label-wrapper {
  margin: 0;
  flex: 0 0 100%;
  text-align: center;
}
[class*="__lg"] {
  display: inline-block;
  width: 100%;
  font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
  margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
  [class*="__lg"] {
    font-size: 2.4rem;
  }
}
.visually-hidden {
  position: absolute;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px);
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}
[class*="stack"] > * {
  margin-top: 0;
  margin-bottom: 0;
}
.stack-small > * + * {
  margin-top: 1.25rem;
}
.stack-large > * + * {
  margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
  .stack-small > * + * {
    margin-top: 1.4rem;
  }
  .stack-large > * + * {
    margin-top: 2.8rem;
  }
}
/* End global styles */
#app {
  background: #fff;
  margin: 2rem 0 4rem 0;
  padding: 1rem;
  padding-top: 0;
  position: relative;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
@media screen and (min-width: 550px) {
  #app {
    padding: 4rem;
  }
}
#app > * {
  max-width: 50rem;
  margin-left: auto;
  margin-right: auto;
}
#app > form {
  max-width: 100%;
}
#app h1 {
  display: block;
  min-width: 100%;
  width: 100%;
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
}
</style>


Solution 1:[1]

Boussadjra Brahim's answer is "alright" but it doesn't actually address the issue. user16362509 is correct, but doesn't answer the question.. What you need to do is yes, annotate the return types of both your data properties and computed properties so TS knows what's going on. Not only that, but it provides stricter type checking if you annotate all types. I don't believe this issue actually occurs when properly using Vue3's composition API, but the problem does, as you can see, exist in options Api. Try: (Or consider using Composition Api). Weirdly enough, when you annotate the return type of a computed method, it understands the context, just quirks of Vue but it's good practice to have the types explicit.

<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";

export default defineComponent({
  name: "App",
  components: {
    ToDoItem,
    ToDoForm,
  },
  data(): { ToDoItems: Array<{id: *string*, label: string, done: boolean}> } {
    return {
      ToDoItems: [
        { id: uniqueId("todo-"), label: "Learn Vue", done: false },
        {
          id: uniqueId("todo-"),
          label: "Create a Vue project with the CLI",
          done: true,
        },
        { id: uniqueId("todo-"), label: "Have fun", done: true },
        { id: uniqueId("todo-"), label: "Create a to-do list", done: false },
      ],
    };
  },
  methods: {
    addToDo(toDoLabel: string): void {
      this.ToDoItems.push({
        id: uniqueId("todo-"),
        label: toDoLabel,
        done: false,
      });
    },
  },
  computed: {
    listSummary(): string {
      const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
        .length;
      return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
    },
  },
});
</script>

Solution 2:[2]

To get type inference in your component you should create it using defineComponent function :

import {defineComponent} from 'vue';

export default defineComponent({
 data() {
    return {
      ToDoItems: [
        { id: uniqueId("todo-"), label: "Learn Vue", done: false },
        {
          id: uniqueId("todo-"),
          label: "Create a Vue project with the CLI",
          done: true,
        },
        { id: uniqueId("todo-"), label: "Have fun", done: true },
        { id: uniqueId("todo-"), label: "Create a to-do list", done: false },
      ],
    };
  },
computed: {
  listSummary() {
      const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
         .length;
        return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
   },
},

})

Solution 3:[3]

You are already using Vue 3. Why not use composition API with script setup for even better typescript support?

Live demo

<template>
  <div id="app">
    <h1>To-Do List</h1>
    <form @submit.prevent="addToDo">
      <input type="text" ref="label" />
      <button type="submit">Add</button>
    </form>
    <h2 id="list-summary">{{ listSummary }}</h2>
    <ul aria-labelledby="list-summary" class="stack-large">
      <li v-for="item in ToDoItems" :key="item.id">
        <input type="checkbox" v-model="item.done" />
        {{ item.id }} {{ item.label }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
interface ToDoItem {
  id: string;
  label: string;
  done: boolean;
}
const label = ref(null)
const ToDoItems = ref<ToDoItem[]>([
  { id: 1, label: 'Learn Vue', done: false },
  { id: 2, label: 'Create a Vue project with the CLI', done: true },
  { id: 3, label: 'Have fun', done: true },
  { id: 4, label: 'Create a to-do list', done: false },
]);
const addToDo = () => {
  ToDoItems.value.push({
    id: ToDoItems.value.length + 1,
    label: label.value.value,
    done: false,
  });
  label.value.value = '';
};
const listSummary = computed(() => {
  return `${ToDoItems.value.filter((item) => item.done).length} out of ${ToDoItems.value.length} items completed`;
});
</script>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Boussadjra Brahim
Solution 3