飽き性の頭の中
Firestoreで適切に型をつけてデータを扱う方法を考えた(TypeScript/Next.js)のサムネイル

Firestoreで適切に型をつけてデータを扱う方法を考えた(TypeScript/Next.js)

2023-02-26に公開

IT技術メモ
# Firebase# Firestore# TypeScript# Next.js

フロントエンドは Next.js で、データは Firestore に保存する構成で軽く開発をしているが、TypeScript の型をいい感じにつけてデータを扱う方法を考えたのでメモする。

Firestore は NoSQL なのでどんな方のデータでも基本的に入れられてしまう。厳密にやるにはセキュリティ・ルールの方でも適切に制御する必要があるが、それは一旦おいておく。

データを操作する Next.js 側で型がある程度あると、データの取得や更新の際に型のチェックができるので、データの整合性を保つことができるはず。

最初に

Firestore にアクセスするためには Collection への参照を持つ必要がある。それを統一することで、特定の Collection に入るデータの型を定義することができる。

users.entity.ts
export const USERS_COLLECTION_KEY = "users";
export const getUsersCollection = (firestore: Firestore) => collection(firestore, USERS_COLLECTION_KEY);

こうすることで、/usersの Collection を触るため使う Collection を使うことができる。

しかし、これだとまだ型の定義ができていない。Firestore はconverterを設定することで、いい感じに型変換をしてくれるので順にやっていく。

Firestore のデータを型を定義する

まずは、Firestore に保存するデータの型を定義する。すべてのデータ構造に共通してcreatedAtupdatedAtを持たせることにしたので、共通化するためにWithTimestampという型を定義している。

加えて、Firestore に保存するデータはidを持っていないが、Next.js 側で使うときにはidがほしいので、snapshotId も含められるようにWithSnapshotIdという型を使って付与できるようにしている。

users.entity.ts
// Firestore内のデータ構造
export type UserDocument = WithTimestamp<{
  name: string;
  email: string;
  profileImageRef?: string;
}>;
// フロントで使うデータ構造
export type UserDocumentWithId = WithSnapshotId<UserDocument>;
types.ts
// 別ファイル
export type WithTimestamp<T> = T & {
  createdAt?: Timestamp; // 作成中一瞬だけundefinedになる
  updatedAt?: Timestamp; // 更新中一瞬だけundefinedになる
};
export type WithSnapshotId<T> = T & { id: string };

converter を定義する

converter では【フロント → Firestore】と【Firestore → フロント】の変換を定義する。

users.entity.ts
export const userConverter: FirestoreDataConverter<UserDocumentWithId> = {
  toFirestore(doc): DocumentData {
    return {
      name: doc.name,
      email: doc.email,
      profileImageRef: doc.profileImageRef,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
    };
  },

  fromFirestore(
    snapshot: QueryDocumentSnapshot<UserDocument>,
    options
  ): UserDocumentWithId {
    const data = snapshot.data(options);
    const entity: UserDocumentWithId = {
      id: snapshot.id,
      name: data.name,
      email: data.email,
      profileImageRef: data.profileImageRef,
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
    };
    return entity;
  },
};

collection に converter を設定する

converter を最初の collection を取得する部分に設定する。

users.entity.ts
export const USERS_COLLECTION_KEY = "users";
export const getUsersCollection = (firestore: Firestore) =>
  collection(firestore, USERS_COLLECTION_KEY).withConverter(userConverter);

withConverterを付ける前

2023 02 27 00 42 49

withConverterを付けた後

2023 02 27 00 43 43

ちゃんとUserDocumentWithIdになっている。

ファイル全体

まとめるとこんな感じ。

users.entity.ts
import {
  DocumentData,
  Firestore,
  FirestoreDataConverter,
  QueryDocumentSnapshot,
  collection,
} from "firebase/firestore";

import { WithSnapshotId, WithTimestamp } from "~/types/firebase";

/**
 * Firestoreに保存するデータの型
 */
export type UserDocument = WithTimestamp<{
  name: string;
  email: string;
  profileImageRef?: string;
}>;

export type UserDocumentWithId = WithSnapshotId<UserDocument>;

export const USERS_COLLECTION_KEY = "users";
export const getUsersCollection = (firestore: Firestore) =>
  collection(firestore, USERS_COLLECTION_KEY).withConverter(userConverter);

export const userConverter: FirestoreDataConverter<UserDocumentWithId> = {
  toFirestore(doc): DocumentData {
    return {
      name: doc.name,
      email: doc.email,
      profileImageRef: doc.profileImageRef,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
    };
  },

  fromFirestore(
    snapshot: QueryDocumentSnapshot<UserDocument>,
    options
  ): UserDocumentWithId {
    const data = snapshot.data(options);
    const entity: UserDocumentWithId = {
      id: snapshot.id,
      name: data.name,
      email: data.email,
      profileImageRef: data.profileImageRef,
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
    };
    return entity;
  },
};

最後に

あとは、この Collection への参照を使えば、必ずデータ構造が保証されることになるので、ある程度安全に使えるのではないかと思っている。

これから開発を進めて、また何か知見があればメモする。

Profile picture

たわ / tawachan

1994年生まれ(29歳)

大学院修士課程(政治学)(2021-2023)

Web開発(2017-)

関連記事

タグ一覧

# 福岡:39# 東京:20# 大学院:13# 移住:10# エナジードリンク:9# Google:8# ブロックチェーン:8# Mac:7# Webエンジニア:7# Apple:6# Firebase:6# Sony:6# サントリー:6# Googleフォト:5# インドネシア:5# 埼玉:5# 英語:5# AWS:4# Canon:4# EOS 8000D:4# Kindle:4# Next.js:4# ZONe:4# ラーメン:4# 京都:4# 仮想通貨:4# 社会人:4# 鴨川シーワールド:4# Docker:3# EOS8000D:3# Lightroom:3# Markdown:3# Pixel:3# React Native:3# a7iii:3# d.school:3# iPad:3# iPad mini:3# アマルティア・セン:3# アメリカ:3# コワーキングスペース:3# セブンイレブン:3# デザイン思考:3# レッドブル:3# ワークショップ:3# 卒業旅行:3# 寿司:3# 紅葉:3# 長崎:3# API:2# ECR:2# ERC20:2# Expo.io:2# Firestore:2# Gatsby.js:2# GitHub Actions:2# Google Drive:2# Kindle Oasis:2# Kindle Paperwhite:2# LINE:2# MacBook Pro:2# NestJS:2# Notion:2# Oculus:2# Oculus Quest:2# Pixel Buds:2# React:2# TypeScript:2# Zotero:2# iPhone:2# pandoc:2# re:Invent:2# zsh:2# かき小屋:2# カフェ:2# ギグワーカー:2# サーチコンソール:2# ジャカルタ:2# スターバックス:2# ステーキ:2# スマートウォッチ:2# ソラマチ:2# チョコレート:2# ニューヨーク:2# バリ島:2# パンとエスプレッソと:2# ヒュッゲ:2# ビーチ:2# ブックスタンド:2# ブログ:2# マクドナルド:2# ミズマチ:2# モンスターエナジー:2# ワイヤレスイヤホン:2# 三千院:2# 両国:2# 修士論文:2# 兵庫:2# 千葉:2# 博多:2# 堀江貴文:2# 宮崎:2# 就活:2# 嵐山:2# 川越:2# 広島:2# 新宿御苑:2# 日米学生会議:2# 有馬温泉:2# 東寺:2# 東浩紀:2# 歴史:2# 民主主義:2# 江ノ島:2# 清澄白河:2# 独自ドメイン:2# 神奈川:2# 神戸:2# 転職:2

©2023 tawachan All Rights Reserved.