shunnami
@shunnami (miffiy)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

【FireStore】getDocsで取得したデータに型定義する方法

解決したいこと

getDocsの型定義をしたいです。型の定義方法を教えてください。

発生している問題・エラー

出ているエラーメッセージを入力

(parameter) doc: QueryDocumentSnapshot<DocumentData>
Argument of type '{ id: string; }' is not assignable to parameter of type 'Book'.
  Type '{ id: string; }' is missing the following properties from type 'Book': title, author

該当するソースコード

import { db } from "./config";
import { collection, getDocs} from "firebase/firestore";

type Book = {
    title:string;
    author:string;
    id:string;
}

export const getAllDocData = async() => {
    const colRef = collection(db,"books");
    const querySnapshot  = await getDocs(colRef);
    let books:Book[]= [];
    querySnapshot.docs.forEach((doc)=>books.push({...doc.data(),id:doc.id}));
    console.log(books);
}

自分で試したこと

型定義を次のようにしたらエラー消えましたが、無理やり型定義をしている感じがします。

// post.types.ts
export type Books = Book[] & Id[]

type Book = {
    title:string;
    author:string;
}
type Id = {
    id:string;
}
// post.ts
import { db } from "./config";
import { collection, getDocs} from "firebase/firestore";
import { Books } from "./types/post.types";

export const getAllDocData = async() => {
    const colRef = collection(db,"books");
    const querySnapshot  = await getDocs(colRef);
    let books:Books = [];
    querySnapshot.docs.forEach((doc)=>books.push({...doc.data(),id:doc.id}));
    console.log(books);
}

また、次のように書くとエラーになる原因がわかりません。ご存知の方がいらっしゃればご教示いただけますと幸いです。

// post.types.ts
- export type Books = Book[] & Id[]
+ export type Books = Book & Id

type Book = {
    title:string;
    author:string;
}
type Id = {
    id:string;
}
// post.ts
import { db } from "./config";
import { collection, getDocs} from "firebase/firestore";
import { Books } from "./types/post.types";

export const getAllDocData = async() => {
    const colRef = collection(db,"books");
    const querySnapshot  = await getDocs(colRef);
  - let books:Books = [];
  + let books:Books[] = [];
    querySnapshot.docs.forEach((doc)=>books.push({...doc.data(),id:doc.id}));
    console.log(books);
}
0

2Answer

doc.data()に対して自動的にBook型を推論させることはできません。
投稿者様はこれらがtitleauthorを持っていることを知っていますが、
doc自体は任意のドキュメントを表す汎用的な型を持っています。

ですので、

{...doc.data(), id: doc.id}

これを見てコンパイラが判断できるのは、せいぜい「idプロパティがある」ことまでです。
そういう意味では、型エラーが出ているのが正常なのです。

それでもなお、ソース外の事情によって「Book型が得られる」ことが保証できるのであれば、
その"知識"を型アサーションを用いてコンパイラに与えます。
(もちろん、万一前提が破られた際には予期しない実行時エラーを招きます。)

次のように関数化するとよいでしょう。
Bookはもともとの定義に基づきます。

const asBook = (doc: DocumentSnapshot): Book => {
    // 必要に応じて検証など
    // ...

    return {...doc.data(), id: doc.id} as Book;
};

const books = querySnapshot.docs.map(asBook);  // Book[]

また、多少コード量は増えますが、**FirestoreDataConverter**を自身で定義することもできます。

そうすれば、withConverterメソッドを介することで、docの型をQueryDocumentSnapshot<Book>とすることができます。
こちらの方がよりオフィシャルです。


私としてはむしろ、Book[] & Id[]でコンパイルが通ってしまうのが不可解なのですが、
それについては次のissueが参考になりました。

今回に関連する部分を嚙み砕くと、
要するにId[] & Book[]型の値というのは、「Idの配列でもBookの配列でもある何か」です。
(Id & Book)[]は「IdでもBookでもある何かの配列」なので、全く異なるものを表しています。

特に前者は、Id[]として振る舞えるので、push(doc)ができるわけですが、
投稿者様がBooksとして表現したかったものでないことは明らかだと思います。

1Like

Comments

  1. @shunnami

    Questioner

    大変丁寧にご回答いただきありがとうございます。

    型アサーション関数の学習をして、型アサーション書くことができました。
    上のコードとは異なるのですが、チャットアプリの中で作成したものになります。
type MessageInfo = {
  username: string,
  message: string,
}

const errorMsg = "エラー"

// MessageInfo型チェック
const asMessageInfo = (doc: DocumentSnapshot) => {
  const messageInfo = doc.data();

  if (typeof messageInfo === "undefined") {
    throw new Error(errorMsg)
  }
  if (!("username" in messageInfo)) {
    throw new Error(errorMsg)
  }
  if (!("message" in messageInfo)) {
    throw new Error(errorMsg)
  }

  return { ...doc.data() } as MessageInfo
}
export const useGetMessageInfo = () => {
  const [messageInfo, setMessageInfo] = useState<MessageInfo[]>([]);

  useEffect(() => {
    /**
     * リアルタイムでコレクションデータを取得
     */
    const colRef = collection(db, "message");
    // 対象のドキュメントに変更から差分があったときのみ動く
    const unSub = onSnapshot(colRef, (querySnapshot) => {
      // get collection data
      try {
        const messageInfo = querySnapshot.docs.map(asMessageInfo);
        setMessageInfo(messageInfo);
      } catch (error) {
        alert(error)
      }
    })
    return unSub;
  }, [])

  return messageInfo
}
1Like

Your answer might help someone💌