Estoy escribiendo un Vue componible usando TypeScript.
Toma un tipo T genérico y una path de un solo parámetro, y devuelve un document ref.
Casi lo tengo funcionando, pero cada vez que intento asignar un valor a la referencia del document , arroja un error como este:
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)Aquí hay una versión recortada del componible:
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; No importa lo que asigne a document.value (cadenas, un objeto vacío, etc.), siempre da un error similar que dice que no se puede asignar al tipo T
Entiendo que el error me dice que el tipo T podría ser cualquier cosa y, por lo tanto, no es seguro asignar estas cosas porque el tipo de las cosas podría no ser compatible con el tipo T
Pero como soluciono este problema? ¿Puedo de alguna manera decirle a TypeScript que el tipo T es compatible con otros tipos?
Acabo de descubrir otra solución.
Dado que Firebase proporciona una interfaz DocumentData , la usé en la referencia del document y el tipo de retorno.
Hacerlo me permitió eliminar el genérico T por completo.
Esto hace que sea una función componible mucho más simple sin la necesidad de crear y pasar mis propias interfaces. Resuelve los errores en este componible y los errores en la plantilla Vue SFC cuando se llama.
Aquí está el código completo:
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;T solo se usa en su tipo de devolución. Su genérico está declarando que la Ref resultante es la elección de T de la persona que llama sin límites.
Incluso si pudiera especificar que T extends { id: string } o que su tipo de retorno es T & { id: string } y puede hacerlo, usando esa sintaxis), tendría que derrotar la tipificación fuerte en algún momento, porque en tiempo de ejecución getDocument nunca sabrá lo suficiente sobre T para afirmar que doc.data() será compatible con el T particular que infiere la llamada getDocument .
Yo elegiría una de estas tres opciones:
Ref<{id: string} | null> , que es de tipo seguro pero lo obligaría a usar la notación ref["key"] para verificar claves desconocidas arbitrarias.getDocument para especificar el tipo correcto, pensando que es mucho más probable que su código introduzca errores que que Firestore le proporcione un tipo de datos inesperado. Necesitaría usar as any para mostrar a TypeScript que está eliminando deliberadamente la seguridad de tipos al asignar document.value .getDocument junto con la ruta. El predicado es una función que tomaría un solo argumento y devolvería si su argumento is T , que podría verificar en onSnapshot para asegurarse de que su document sea seguro para convertir a T o falle con la mitigación/error de su diseño. Esto también restringe T para que su genérico no pueda ser absolutamente de ningún tipo. Vea el ejemplo a continuación. 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); }