2

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>
4
  • @WilliamLohan, I'm willing to help. Could you create a runnable minimal reproducible example (on codesandbox.io or similar), providing the minimal code to repro the bug? Make sure package.json matches what you have in your project. Without this info, chances are nobody can help you, as we don't have enough to repro. Version of typescript & vue are important. It also matters if you're using vite vs @vue/cli and/or any IDE plugins for Vue. If you use VSCode, you could repro on vscode.dev instead of codesandbox.io Commented Apr 16, 2022 at 2:28
  • I believe this might be related to your IDE & ts configuration. Your code seems correct, so try to change in your tsconfig.json: noImplicitThis: false (having it true could cause some issues when compiling in your IDE). If it did not resolve it, try to also have vetur.validation.interpolation if you are using vetur. Commented Apr 16, 2022 at 14:28
  • I was unable to create a reproduction project, but I found it was only effecting components with out props and adding props: {} as Readonly<ComponentPropsOptions<Record<string, never>>> works Commented Apr 18, 2022 at 20:31
  • You might wanna take a look at: stackoverflow.com/a/59600275/4920105 The shims-vue.d.ts is named there with the content thats needed. Commented Apr 21, 2022 at 12:23

2 Answers 2

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>
Sign up to request clarification or add additional context in comments.

Comments

0

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>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.