📑 오류기록

[ FireBase ERROR ] Argument of type 'DocumentData' is not assignable to parameter of type 'Topic'.

Po_tta_tt0 2022. 12. 27. 21:17
반응형

 

 

0. 오류 상황 발생

 

firestore.ts

export const getAllDocsOnCollections = async (collecionName: string) => {
  const querySnapshot = await getDocs(collection(db, collecionName));
  return querySnapshot;
};

getDocs로 collection의 모든 문서들을 받아와 반환한다.

 

 

CraftRoom.tsx

  const setTopics = async () => {
    const topicArray: Topic[] = [];

    (await getAllDocsOnCollections(FIRESTORE_COLLECTIONS.topics)).forEach(
      (doc) => {
        const topic = doc.data();
        topicArray.push(topic); // 💥 Argument of type 'DocumentData' is not assignable to parameter of type 'Topic'.
      }
    );

    getTopics((prev) => [...topicArray]);
  };

setTopics 함수가 호출되면 getAllDocsOnCollection함수로 모든 문서를 받아온 후.
forEach를 돌려 topicArray에 받아온 data (여기서는 topic)을 차곡차곡 넘겨주고, getTopics라는 useState 값을 변환시켜주는 코드다.
집중해야 할 부분은
topicArray.push(topic). 이 부분이 바로 에러가 발생한 부분이다.

 

 

topics.ts

export interface Balance {
  a: string;
  b: string;
}

export interface Topic {
  balances: Balance[];
  title: string;
}

 

 

1. 추론

왜 문제가 발생했을까?

분명히 같은 구조의 객체고, (typeof로 찍어봤다.)
await getAllDocsOnCollections(FIRESTORE_COLLECTIONS.topics)) 대신 mock 데이터를 forEach로 돌리면서 확인해봤을 때는 정상적으로 작동했다.

따라서 fireStore에 위치한 collection의 문서들을 받아올 때. 알 수 없는 이유로 타입 추론이 안된다는 가정을 가지고 문제를 살펴봤다.

 

 

 

2. 시도

Firestore typescript does not work as documentation. 이슈를 발견하게 되었고, 이슈 내용이 현재 내 문제와 같은 것을 확인했다.

 

Check this code sandbox, you will see that ref is not assignable to useFirestoreQueryData as stated in documentation.
Type 'DocumentData' is not assignable to type 'Product'.
Am I doing something wrong or is there work around for this in the meanwhile?

 

== 나와 같이 타입 추론이 안되는 문제 확인

 

 

firebase 공식문서

withConverter()에서 T 유형의 사용자 객체를 Firestore 데이터로 변환하는 데 사용하는 변환기이다.
변환기를 사용하면 Firestore에서 객체를 저장하고 검색할 때 일반 유형 인수를 지정할 수 있다.

 

export declare interface FirestoreDataConverter<T>
fromFirestore(스냅샷, 옵션) Firestore SDK에서 호출하여 Firestore 데이터를 유형 T의 객체로 변환합니다. snapshot.data(options) 를 호출하여 데이터에 액세스할 수 있습니다.
toFirestore(모델 객체) T 유형의 사용자 정의 모델 객체를 일반 JavaScript 객체로 변환하기 위해 Firestore SDK에 의해 호출됩니다(Firestore 데이터베이스에 직접 쓰기에 적합). merge 및 mergeFields 와 함께 set() 을 사용하려면 toFirestore() 를 PartialWithFieldValue 로 정의해야 합니다. WithFieldValue 유형은 T 를 확장하여 deleteField() 와 같은 FieldValues를 속성 값으로 사용할 수도 있습니다.
toFirestore(modelObject, 옵션) T 유형의 사용자 정의 모델 객체를 일반 JavaScript 객체로 변환하기 위해 Firestore SDK에 의해 호출됩니다(Firestore 데이터베이스에 직접 쓰기에 적합). setDoc() 과 함께 사용 및 merge:true 또는 mergeFields . PartialWithFieldValue 형식은 Partial 를 확장하여 arrayUnion() 과 같은 FieldValues를 속성 값으로 사용할 수 있도록 합니다. 또한 중첩 필드를 생략할 수 있도록 하여 중첩된 Partial 을 지원합니다.

등등이 있었다.
자세한 내용은 공식문서를 확인하기.

 

예시 중

const postSnap = await firebase.firestore()
  .collection('posts')
  .withConverter(postConverter) // 🤔 이걸 이용할 수 있지 않을까?
  .doc().get();
const post = postSnap.data();
if (post !== undefined) {
  post.title; // string
  post.toString(); // Should be defined
  post.someNonExistentProperty; // TS error
}

을 확인.

 

그러다가 다시 issue를 확인했는데

 

Hey! There is a simple solution:

import { CollectionReference } from 'firebase/firestore'

const q = query(
  collection(db, 'task-lists') as CollectionReference<TaskListModel> // 🍀
)

useFirestoreQuery(['task-lists'], q)

오!

 

 

적용해보기

firestore.ts

export const getAllDocsOnCollections = async (collecionName: string) => {
  const querySnapshot = await getDocs(
    collection(db, collecionName) as CollectionReference<Topic> // 😀
  );
  return querySnapshot;
};

 

이 부분만 바꿔주니 정상적으로 동작한다
collection에서 문서를 받아올 때, CollectionReference<Topic>으로 받아온다는 것을 명시하니 타입을 잘 추론해준다.
고마워 따봉이슈야

 

그렇다면.. CollectionReference와 withConverter은 어떤 차이가 있는 걸까?

 

 

 

firestore > index.d.ts 까보기

export declare class CollectionReference<T = DocumentData> extends Query<T> {
    /** The type of this Firestore reference. */
    readonly type = "collection";
    private constructor();
    /** The collection's identifier. */
    get id(): string;
    /**
     * A string representing the path of the referenced collection (relative
     * to the root of the database).
     */
    get path(): string;
    /**
     * A reference to the containing `DocumentReference` if this is a
     * subcollection. If this isn't a subcollection, the reference is null.
     */
    get parent(): DocumentReference<DocumentData> | null;
    /**
     * Applies a custom data converter to this `CollectionReference`, allowing you
     * to use your own custom model objects with Firestore. When you call {@link
     * addDoc} with the returned `CollectionReference` instance, the provided
     * converter will convert between Firestore data and your custom type `U`.
     *
     * @param converter - Converts objects to and from Firestore.
     * @returns A `CollectionReference<U>` that uses the provided converter.
     */
    withConverter<U>(converter: FirestoreDataConverter<U>): CollectionReference<U>; // 👀
    /**
     * Removes the current converter.
     *
     * @param converter - `null` removes the current converter.
     * @returns A `CollectionReference<DocumentData>` that does not use a
     * converter.
     */
    withConverter(converter: null): CollectionReference<DocumentData>; 
}

CollectionReference가 type을 받아서 타입을 명시해주는 것 같은데..

내부에 withConverter이 들어있다.

Applies a custom data converter to this `CollectionReference`, allowing you to use your own custom model objects with Firestore.
When you call {@link addDoc} with the returned `CollectionReference` instance, the provided converter will convert between Firestore data and your custom type `U`.

 그러니까 CollectionReference는 custom model object을 Firestore에서 이용할 수 있게 하고,

우리가 addDoc과 CollectionReference를 같이 쓰면 fFirestore data를 내 커스텀 타입인 <U>로 변환해준다는 것.

 

 

 

withConverter 써보기

firestore.ts

const firestoreConverter = <T extends Record<string, any>>() => ({
  toFirestore({ id, ...data }: PartialWithFieldValue<T>): DocumentData {
    return data;
  },
  fromFirestore(
    snapshot: QueryDocumentSnapshot<T>,
    options: SnapshotOptions
  ): T {
    const data = snapshot.data(options);
    return {
      id: snapshot.id,
      ...data,
    };
  },
});

export const getAllDocsOnCollections = async (collecionName: string) => {
  const querySnapshot = await getDocs(
    collection(db, collecionName).withConverter(firestoreConverter<Topic>())
  );
  return querySnapshot;
};

🙀

완벽히 알고 사용하는 것이 아니라 오류를 해결하기 위해 여기저기 뒤적거리다가 찾은 방법이기 때문에, withConverter을 이렇게 쓰지 않아도 될 수 있다. 분명 더 좋은 방법이 있겠지

아무튼..지금은
CollectionReference 쓰자

반응형