Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

410
Views
How to assign a value to a ref that has a generic type?

I am writing a Vue composable using TypeScript.

It takes in a generic type T and a single paramter path, and returns a document ref.

I've almost got it working but whenever I try to assign a value to the document ref it throws an error like this:

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)

Here is a trimmed down version of the composable:

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;

It doesn't matter what I assign to document.value (strings, an empty object, etc) it always gives a similar error saying it is not assignable to type T.

I understand the error is telling me the type T could be anything, and therefore it's not safe to assign these things because the type of the things might be not be compatible with type T.

But how do I solve this problem? Can I somehow tell TypeScript that type T is compatible with other types?

over 4 years ago · Santiago Trujillo
2 answers
Answer question

0

I just discovered another solution.

Since Firebase provides a DocumentData interface, I used that in the document ref and return type.

Doing so allowed me to remove the T generic entirely.

This makes it a much simpler composable function without the need to create and pass in my own interfaces. It resolves the errors in this composable, and the errors in the Vue SFC template when called.

Here is the full code:

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;
over 4 years ago · Santiago Trujillo Report

0

T is only used in your return type. Your generic is declaring that the resulting Ref is the caller's choice of T with no boundaries.

Even if you could specify that T extends { id: string } or that your return type is T & { id: string }—and you can, using that syntax—you'd have to defeat strong typing at some point, because at runtime getDocument won't ever know enough about T to assert that doc.data() will be compatible with the particular T that the getDocument call infers.

I'd pick one of these three options:

  • Remove the generic and return Ref<{id: string} | null>, which is type-safe but would force you to use ref["key"] notation to check arbitrary unknown keys.
  • Keep the generic and trust your calls to getDocument to specify the right type, figuring that it's much more likely for your code to introduce bugs than for Firestore to give you an unexpected data type. You'd need to use as any to show TypeScript you're deliberately dropping type safety in assigning document.value.
  • Use a type predicate that you pass into getDocument along with the path. The predicate is a function that would take a single arg and return whether its arg is T, which you could check in onSnapshot to ensure that your document is safe to cast to T or fail with the mitigation/error of your design. This also constrains T so your generic can't be absolutely any type. 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

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!