typescriptvue.jsfetch-api

Understanding FetchAPI in vue + TS


I am developing a simple notes app. I need to fetch data from the server and then render it. I have a bunch of ready components and working backend, but kinda struggle with fetching data. When I', trying to build it gives me errors. Also would like to ask if ts would type cast the response correctly as there are some extra fields.

App.vue:

<script setup lang="ts">
import Form from "./components/Form.vue";
import NoteItem from "./components/NoteItem.vue";

type Note = {
    title: string;
    content: string;
}

let loaded = false;
let noNotes = false;
const telegramId = "test"
const notes: Promise<Note[]> = fetch(`http://localhost:3000/notes/${telegramId}`)
    .then((res) => {
      if (res.status === 404) {
        noNotes = true;
      }

      return res.json() as Note[];
    })
    .then(() => loaded = true)
    .catch((err) => console.log(err));
</script>

<template>
  <div class="flex flex-col justify-center items-center justify-items-center h-screen space-x-4 gap-0">
    <h1 class="font-bold h-max text-6xl font-serif underline decoration-sky-500">Notes</h1>
    <Form/>
    <div class="grid grid-cols-3 w-1/3 mt-8 gap-4">
      <div v-if="loaded">
        <div v-for="note in notes">
          <NoteItem :title="note.title" :content="note.content"/>
        </div>
      </div>
      <div v-else>
        <h2 v-if="noNotes"></h2>
        <h2 v-else>Fetching...</h2>
      </div>
    </div>
  </div>
</template>

<style scoped>
</style>

Response example:

[
    {
        "ID": 1,
        "CreatedAt": "2024-10-16T18:17:25.141109Z",
        "UpdatedAt": "2024-10-16T18:17:25.141109Z",
        "DeletedAt": null,
        "TelegramId": "test",
        "Title": "Test3",
        "Content": "Test3"
    }
]

Errors:

src/App.vue:13:7 - error TS2322: Type 'Promise<boolean | void>' is not assignable to type 'Promise<Note[]>'.
  Type 'boolean | void' is not assignable to type 'Note[]'.
    Type 'boolean' is not assignable to type 'Note[]'.

13 const notes: Promise<Note[]> = fetch(`http://localhost:3000/notes/${telegramId}`)
         ~~~~~

src/App.vue:19:14 - error TS2352: Conversion of type 'Promise<any>' to type 'Note[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'Promise<any>' is missing the following properties from type 'Note[]': length, pop, push, concat, and 28 more.

19       return res.json() as Note[];
                ~~~~~~~~~~~~~~~~~~~~

src/App.vue:32:34 - error TS2339: Property 'title' does not exist on type 'string | (<TResult1 = Note[], TResult2 = never>(onfulfilled?: ((value: Note[]) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<...>) | null | undefined) => Promise<...>) | (<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<...>) | .....'.
  Property 'title' does not exist on type 'string'.

32           <NoteItem :title="note.title" :content="note.content"/>
                                    ~~~~~

src/App.vue:32:56 - error TS2339: Property 'content' does not exist on type 'string | (<TResult1 = Note[], TResult2 = never>(onfulfilled?: ((value: Note[]) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<...>) | null | undefined) => Promise<...>) | (<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<...>) | .....'.
  Property 'content' does not exist on type 'string'.

32           <NoteItem :title="note.title" :content="note.content"/>

Solution

  • Answers are scattered in the comments, let me summarize them.

    In your code, notes is Promise<Note[]>, which is not iterable. fetch is asynchronous. When the component is rendered, fetch may not have even started yet.

    You can add a loading placeholder like this:

    <script lang="ts" setup>
    const notes = ref<Note[] | undefined>(); // Use `ref` to ensure reactivity
    fetch(...).then(...)
    </script>
    
    <template>
      <p v-if="typeof notes === 'undefined'">loading...</p>
      <div> <!-- `notes` is narrowed to `Note[]` here --> </div>
    </template>
    

    And you can use await to make it tidier, but the component will become asynchronous. Then the component itself don't need to manage the loading placeholder, but the parent component do by <Suspense>. Like:

    <template>
      <Suspense>
        <NotesArea>
        <template #fallback>
          Loading...
        </template>
      </Suspense>
    </template>
    

    When using await, remember that App.vue can't be asynchronous. You can extract this into another SFC.

    Performance note: if the number of elements of notes is large, shallowRef will have a better performance.