Using Firestore with TypeScript in the v9 SDK

A nice little coding design pattern that will boost your Firestore productivity and make your code safe with TypeScript

Jamie Curnow
JavaScript in Plain English
5 min readNov 4, 2021

Photo by Ian Stauffer on Unsplash

TL;DR: Just check out this repo for an example of the code:

Let’s Code!

So I’ve been playing with the new Firebase/Firestore V9 SDK recently and have come up with a very simple and very minimal way to make it play nice with TypeScript.

The only thing we’re going to do is to create an abstraction that maps all of the possible collections in our DB, and returns a typed Firestore collection reference. Since I’m a fan of Vue and the ‘composables’ coding paradigm, I’m going to put this abstraction in a directory named composables and in a file named useDb.ts . The first thing we’ll do in that file is to initialize the firebase app, and get a reference to the Firestore db:

// /composables/useDb.ts// Get the imports
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
// Init the firebase app
export const firebaseApp = initializeApp({
apiKey: '### FIREBASE API KEY ###',
authDomain: '### FIREBASE AUTH DOMAIN ###',
projectId: '### CLOUD FIRESTORE PROJECT ID ###'
})
// Export firestore incase we need to access it directly
export const firestore = getFirestore()

Next, we’re going to make a little function that will take a collection name and spit out a Firestore collection reference. This reference can be passed to getDoc or getDocs etc for read/writing data:

// /composables/useDb.ts// Get the imports
import { initializeApp } from 'firebase/app'
import { getFirestore, collection } from 'firebase/firestore'
// Init the firebase app
export const firebaseApp = initializeApp({
apiKey: '### FIREBASE API KEY ###',
authDomain: '### FIREBASE AUTH DOMAIN ###',
projectId: '### CLOUD FIRESTORE PROJECT ID ###'
})
// Export firestore incase we need to access it directly
export const firestore = getFirestore()
// This is just a helper to add the type to the db responses
const createCollection = (collectionName: string) => {
return collection(firestore, collectionName)
}

Right now that function really doesn’t do much and is kind of pointless, but here comes the magic... We’re going to make that function a type generic and add the correct type to the collection reference that it returns:

// Get the imports
import { initializeApp } from 'firebase/app'
import { getFirestore, CollectionReference, collection, DocumentData } from 'firebase/firestore'
// Init the firebase app
export const firebaseApp = initializeApp({
apiKey: '### FIREBASE API KEY ###',
authDomain: '### FIREBASE AUTH DOMAIN ###',
projectId: '### CLOUD FIRESTORE PROJECT ID ###'
})
// Export firestore incase we need to access it directly
export const firestore = getFirestore()
// This is just a helper to add the type to the db responses
const createCollection = <T = DocumentData>(collectionName: string) => {
return collection(firestore, collectionName) as CollectionReference<T>
}

Pretty simple. Pretty concise.

Now we’ll import all of our Types from wherever they are defined, and export a bunch of constants that run the createCollection function with the correct collection path and type:

// Get the imports
import { initializeApp } from 'firebase/app'
import { getFirestore, CollectionReference, collection, DocumentData } from 'firebase/firestore'
// Init the firebase app
export const firebaseApp = initializeApp({
apiKey: '### FIREBASE API KEY ###',
authDomain: '### FIREBASE AUTH DOMAIN ###',
projectId: '### CLOUD FIRESTORE PROJECT ID ###'
})
// Export firestore incase we need to access it directly
export const firestore = getFirestore()
// This is just a helper to add the type to the db responses
const createCollection = <T = DocumentData>(collectionName: string) => {
return collection(firestore, collectionName) as CollectionReference<T>
}
// Import all your model types
import { User } from 'src/types/User'
import { Author } from 'src/types/Author'
import { Book } from 'src/types/Book'
// export all your collections
export const usersCol = createCollection<User>('users')
export const authorsCol = createCollection<Author>('authors')
export const booksCol = createCollection<Book>('books')

You now just have one list to maintain of collection names and their types!

So how do I use this?

Well, it’s as simple as it looks! Say we wanted to add a new user to the db, we would just import usersCol from useDb and use that as the first argument to Firestore’s setDoc function:

// /someOtherFile.tsimport { doc, setDoc } from '@firebase/firestore'
import { usersCol } from './composables/useDb'
export const setJamiesUser = async () => {
const userRef = doc(usersCol, 'user_12345')
await setDoc(userRef, {
age: 30,
firstName: 'Jamie',
lastName: 'Curnow'
})
}

If you try this out, you’ll see that we get type hints and compiler errors if we give setDoc the wrong type of data for the user!

Another example: getting data… Let’s say we want to get all of the documents from the books collection in the db and we want to console.log() each of the book’s titles:

// /anotherFile.tsimport { getDocs } from '@firebase/firestore'
import { booksCol } from './composables/useDb'
export const logBookTitles = async () => {
const bookDocs = await getDocs(booksCol)
bookDocs.docs.forEach((bookDoc) => {
const book = bookDoc.data()
console.log(book.title)
})
}

Here typescript knows that bookDoc.data() returns an object of type Book and lets us log the title of the book. 🥳

Using it this way will also work for deeply nested keys when doing updates to documents which is absolutely awesome:

import { doc, updateDoc } from '@firebase/firestore'
import { booksCol } from './composables/useDb'
export const updateBook = async () => {
const bookDocRef = doc(booksCol, 'book_12345')
await updateDoc(bookDocRef, {
'meta.created': new Date().toISOString()
})
}

In that example meta.created is typed! Amazing!

😍 Typescript

Wrapping up

This is such a nice feature of the new V9 Firestore SDK and the Firestore team have done a fantastic job of building it 🙌.

If you want to see a minimal repo that implements this pattern then checkout and star:

Don’t forget to give this story a little clap if you found it useful — your claps keep me posting content and give me a little boost to write more each time I see one come through!

Support your creators!

If you found this story useful and would like me to keep writing useful content, please consider supporting me on Patreon 🤗

More content at plainenglish.io

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Written by Jamie Curnow

I am a javascript developer living in beautiful Cornwall, UK. Ever curious, always learning.

Responses (6)

What are your thoughts?