By default, Firestore fetches documents as DocumentData type. This is not very handy especially with typescript since it doesn’t allow us to take advantage of autocompletion . We would like to receive data from firestore as custom defined types. Fortunately firebase provides a data converter that will make our lives easier.

Simple converter

This example shows how to convert data from firestore to any type that we need. “Simple converter” provides types when fetching the document but it doesn’t ensure the correct shape of received documents. This could be a desired behavior for example on the client side (browser) where we want our app to continue working even if received data is incomplete.

First thing that we need is a generic data converter. We will use it later to define collections references.

export const getSimpleConverter = <T extends DocumentData>(): FirestoreDataConverter<T> => ({
    toFirestore: (item: T) => item,
    fromFirestore: (snapshot: QueryDocumentSnapshot<T>, options) => snapshot.data(options)
});

Let’s say that we have “posts” collection and each post document should have flowing type:

type Post = {
    authorId: string;
    content: string;
    createdAtTimestamp: number;
};

Let’s create post collection reference with simple converter:

const postCollectionRef = collection(getFirestore(), "posts" ).withConverter(
    getSimpleConverter<Post>()
);

Now when fetching single or multiple posts we will receive the correct Post or Post[] type. Following example shows fetching data in the browser but it will work similarly with the admin SDK in the cloud or node server.

// posts: Post[];
const posts = (await getDocs(postCollectionRef)).docs.map((s) => s.data());

Most of the time this is good enough but in some cases we need to make sure that the document that we receive has the correct shape. For this we need a parser.

Parsed converter

In this example we will create a converter with a parser. This means that if the document doesn’t have the correct shape, fetching or posting it will throw an error. This is a great converter to use on the backend, where most of the time we should stop the execution when data received from the server is incomplete or corrupted.

Once again let’s assume we have a collection of posts with this shape :

type Post = {
    authorId: string;
    content: string;
    createdAtTimestamp: number;
};

This time we need a parser that will make sure data has the correct shape. For this I will use the zod package.

const postParser = (post: unknown): Post =>
    z
        .object({
            authorId: z.string().uuid(),
            content: z.string().min(1),
            createdAtTimestamp: z.number()
        })
        .parse(post);

Our converter has to use a parser when fetching or posting data:

type ParsedConverter = <T extends DocumentData>(
    parser: (doc: unknown) => T
) => FirestoreDataConverter<T>;

export const getParsedConverter: ParsedConverter = (parser) => ({
    toFirestore: (item: unknown) => parser(item),
    fromFirestore: (snapshot: QueryDocumentSnapshot) => parser(snapshot.data())
});

Let’s create post collection reference with parser :

const postCollectionRef = collection(getFirestore(), Collections.posts).withConverter(
    getParsedConverter(postParser)
);

Now we are safe to assume that the documents received from the firestore will always have the correct shape. If not, zod will throw an error. Our parser will also make sure we won’t save documents with incorrect shapes!

// you can be certain that:
// posts: Post[];
const posts = (await getDocs(postCollectionRef)).docs.map((s) => s.data());

// Argument of type '{ badData: string; }' is not assignable to parameter of type 'WithFieldValue<Post>'.
// zod will throw an error
addDoc(postCollectionRef, { badData: 'badData' });