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?
CodePudding user response:
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 useref["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 useas any
to show TypeScript you're deliberately dropping type safety in assigningdocument.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 argis T
, which you could check inonSnapshot
to ensure that yourdocument
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);
}