What is the best way to set a value for a generic-typed ref?

I've been working on a Vue composable using TypeScript that utilizes a generic type T and accepts a single parameter path, ultimately returning a reference to a document.

While I have made progress, I keep encountering an error when trying to set a value to the document ref:

Type '{ id: string; }' is not assignable to type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to '{ id: string; }'.ts(2322)

Below is a condensed version of the composable code:

import { ref, Ref } from "vue";
import { projectFirestore } from "@/firebase/config";
import { doc, onSnapshot } from "firebase/firestore";

const getDocument = <T>(
  path: string
): {
  document: Ref<T | null>;
} => {
  const document = ref(null) as Ref<T | null>;
  const docRef = doc(projectFirestore, path);
  onSnapshot(docRef, (doc) => {
    document.value = { //<-- Error here on "document.value"
      ...doc.data(),
      id: doc.id,
    };
  });
  return { document };
};

export default getDocument;

Despite experimenting with various assignments to document.value such as strings or {} objects, I continue to receive similar errors indicating incompatibility with type T.

I recognize that the issue lies in TypeScript's uncertainty regarding type T's compatibility with other types. Is there a way to inform TypeScript that type T can indeed work with these various types?

Answer №1

T is specifically designated for the return type in this scenario. The generic declaration indicates that the resulting Ref can be any type chosen by the caller as T, without any restrictions or constraints.

Even if you were to define T extends { id: string } or set your return type as T & { id: string }—possible using appropriate syntax—you would still encounter challenges with TypeScript's strong typing since, at runtime, it's impossible for getDocument to determine enough information about T to ensure compatibility between doc.data() and the inferred T from the getDocument call.

Here are three possible alternatives:

  • Remove the generic and have a return type of Ref<{id: string} | null>, ensuring type safety but necessitating the use of ref["key"] notation for checking unknown keys.
  • Maintain the generic and rely on calls to getDocument to specify the correct type, assuming that errors introduced in your code are more likely than Firestore returning an unexpected data type. Using as any would be necessary to indicate to TypeScript that you are intentionally sacrificing type safety when assigning document.value.
  • Implement a type predicate to pass into getDocument alongside the path. This predicate, a function taking one argument and indicating whether its arg is T, can be checked within onSnapshot to confirm that your document can safely be cast to T or handle failures accordingly. Additionally, this approach confines T to prevent arbitrary types in your generic. See example below.

const getDocument = <T>(
  path: string,
  docPredicate: (doc: any) => doc is T,  // accept predicate
): {
  document: Ref<T & {id: string} | null>;
} => {
  const document = ref(null) as Ref<T & {id: string} | null>;
  const docRef = doc(projectFirestore, path);
  onSnapshot(docRef, (doc) => {
    const data = doc.data(); // extract to local variable
    if(docPredicate(data)) { // convince TS data is type T
      document.value = {
        ...data,             // integrate as expected
        id: doc.id,
      };
    }
  });
  return { document };
};

interface Foo { a: string, b: number }
function isFoo(doc: any): doc is Foo {
  // TODO: check that doc has {a: string, b: number}
  return true;
}

const fooDoc = getDocument("bar", isFoo);
if (fooDoc.document.value) {
  console.log(fooDoc.document.value.a);
  console.log(fooDoc.document.value.id);
}

Playground before => Playground after

Answer №2

After some exploration, I stumbled upon a different approach.

By leveraging the DocumentData interface provided by Firebase, I decided to incorporate it in both the document reference and return type.

This choice enabled me to eliminate the need for the generic T altogether.

The result is a more streamlined and straightforward function that does not require custom interfaces to be created and passed in. This successfully resolves the issues within this composable and eliminates errors in the Vue SFC template when utilized.

Below is the complete code snippet:

import { ref, Ref } from "vue";
import { projectFirestore } from "@/firebase/config";
import {
  doc,
  onSnapshot,
  DocumentSnapshot,
  DocumentData,
} from "firebase/firestore";

const getDocument = (
  path: string
): {
  document: Ref<DocumentData | null>;
} => {
  const document = ref<DocumentData | null>(null);
  const docRef = doc(projectFirestore, path);
  onSnapshot(docRef, (doc: DocumentSnapshot<DocumentData>) => {
    if (doc.data()) {
      document.value = {
        ...doc.data(),
        id: doc.id,
      };
    }
  });
  return { document };
};

export default getDocument;

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unlock the potential of Vue custom props within the asyncData function in your nuxtjs project!

I have a special function in my project that serves as a plugin import Vue from 'vue' function duplicateText(text) { var input = document.createElement('input'); input.setAttribute('value', text); document.body.a ...

How can I arrange a specific array position in Vuejs according to the Id value?

<div v-for="(item, index) in gr" :key="space.id" class="val-name"> </div> After making a few modifications to the API call logic, I was able to achieve my desired outcome. However, the issue lies in the fact that ...

Guide to utilizing Terser in conjunction with webpack

I am currently using Webpack 6.10.2 in combination with Vue 3.9.3. I encountered an issue with Uglify.js when running npm run build due to its inability to handle ES6 code. To work around this problem, I followed the recommendation of removing Uglify fro ...

Having issues with Vue js increment operator not yielding the desired results

What could be causing the below Vue.js code to output 102? <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> </head> <body> <div id = "intro&qu ...

Code error TS1005 indicates that an ampersand is expected

My project currently utilizes TypeScript version 3.9.7 and I'm encountering an issue with the following code snippet. This code works perfectly fine in TypeScript version 4.2.3. export namespace Abcd { export type AbcdOneParams = [xAxis: number, y ...

Vue 3 app encountering error due to element having an 'any' type implicitly

I want to incorporate lucidev icons into my component, but I am fairly new to typescript. You can find the lucidev icons here: https://github.com/lucide-icons/lucide <template> <component :is="icon" /> </template> <script ...

Issues encountered while attempting to utilize the delete function within a CRUD process involving Angular 8, Laravel, and JWT authentication

Currently, I am developing a Single Page Application using Angular 8 on the frontend and Laravel on the backend. The frontend consists of a form with options for users to either edit or delete a user. When the delete button is clicked, I capture the curren ...

The state array is rejecting the value from the other array, resulting in null data being returned

I am currently attempting to extract image URLs from an HTML file input in order to send them to the backend and upload them to Cloudinary. However, I am facing an issue where despite having the imagesArr populated with images, setting the images state is ...

Can you explain how to specify individual keys in an object literal in TypeScript?

So I've been working with this data structure export interface StoreData { msdb: {[tableName: string]: List<StoreModel>}; } However, I'm looking to restrict and enable auto-completion for specific string values in my tableName field. ...

How to Publish an Angular 8 Application on Github Pages using ngh

Currently in my angular 8 project, I am encountering the following issue while running the command: ole@mkt:~/test$ ngh index.html could not be copied to 404.html. This does not look like an angular-cli project?! (Hint: are you sure that you h ...

What is the proper way to input a Response object retrieved from a fetch request?

I am currently handling parallel requests for multiple fetches and I would like to define results as an array of response objects instead of just a general array of type any. However, I am uncertain about how to accomplish this. I attempted to research "ho ...

Working with e.charcode in TypeScript allows for easy access to

I'm having trouble understanding why this code snippet is not functioning as expected. const addRate = (e: { charCode: KeyboardEvent }) => { if (e.charCode >= 48) { ... } } The error message I receive states: 'Operator '>=& ...

Arrays nested in Laravel validator responses

Is there a way to customize the validator responses to expand nested arrays in Laravel? By default, Laravel uses the "dot notation" for error messages like this: [ 'organisation.name' => 'required|max:60|min:3', ...

Determining the length and angle of a shadow using X and Y coordinates

I'm currently tackling the task of extracting data from a file generated by a software program that has the ability to add a shadow effect to text. The user interface allows you to specify an angle, length, and radius for this shadow. https://i.stack ...

Tips for utilizing method overload in TypeScript with a basic object?

Looking at this code snippet: type Foo = { func(a: string): void; func(b: number, a: string): void; } const f: Foo = { func(b, a) { // ??? } } An error is encountered stating: Type '(b: number, a: string) => void' is not assign ...

Display images in a list with a gradual fade effect as they load in Vue.js

In my Vue project, I am looking to display images one at a time with a fading effect. I have added a transition group with a fade in effect and a function that adds each image with a small delay. However, I am facing an issue where all the images show up ...

Utilize an array of observables with the zip and read function

I'm struggling with putting an array of observables into Observable.zip. I need to create a function that reads values from this dynamically sized array, but I'm not sure how to go about it. Does anyone have any suggestions? import {Observable} ...

Rounded Doughnut Chart using Vue3 and vue-chart-3 for visually appealing data representation

I'm struggling to customize the arcs in my donut chart graphic. I want to separate the start and end arcs from the changing arcs but can't figure out how. Currently, it looks like this: I'm having difficulty modifying the arc sections of th ...

Unable to generate paths in Ionic 3

I am trying to view the actual route on the URL, but I'm having trouble figuring out exactly what needs to be changed. I've been referring to the documentation at: https://ionicframework.com/docs/api/navigation/IonicPage/ However, I keep encoun ...

Identify when a div reaches the end of the screen using CSS

I've developed a custom dropdown feature using Vue 2 and TypeScript to meet our specific requirements (without relying on jQuery). The dropdown functions correctly, opening the options list downwards when the main box is clicked. One enhancement I a ...