circuit

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




Photo by Ian Stauffer on Unsplash

TL;DR: Just check out this repo for an example of the code: GitHub - JamieCurnow/firebase-v9-typescript-example: An example, minimal repo showing how to use…

If you’re looking for a way to add TypeScript to the V8 firebase SDK then check out my other articles:

Using Firestore With Typescript

Using Firestore with more Typescript

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.ts

import { doc, setDoc } from '[@firebase/firestore](http://twitter.com/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.ts

import { getDocs } from '[@firebase/firestore](http://twitter.com/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](http://twitter.com/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😍 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: GitHub - JamieCurnow/firebase-v9-typescript-example: An example, minimal repo showing how to use… *An example, minimal repo showing how to use the firebase v9 SDK with types :) - GitHub …*github.com

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 🤗 Jamie Curnow is creating Software | Patreon




Continue Learning