Search code examples
typescriptfirebasegoogle-cloud-firestoretypescript-typingsmobx-state-tree

Firebase + Typescript - issue with ID property and withConverter (creating an new Document that don't have ID property)


Let me try to explain the situation:

  1. To create an new document in firestore, I'm using add method from collection
  2. To keep type safe, using typescript, I'm using withConverter
  3. When I'm creating an new document, I don't have ID property on my object
  4. After the object is created, I have the ID property

bdw.. I'm using Mobx-state-tree

The code

Let me describe step by step

Company model defined by mobx-state-tree

const CompanyModel = types.model({
  id: types.identifier,
  name: types.string
});

Types that I will needed

After I call add method, CompanyReference is that what I get from firestore

type CompanyReference = firebase.firestore.DocumentReference<
  Instance<typeof CompanyModel>
>;

TCompanySnapshot is that what I'm sending to the firestore by using add method

type TCompanySnapshot = Omit<SnapshotIn<typeof CompanyModel>, "id">;

Converting from and to firestore by using withCOnverter

  • Send data to firestore, just user the regular TCompanySnapshot object
  • Receive some object from firestore (now I have the ID property), convert into CompanyModel
const createCompanyConverter: firebase.firestore.FirestoreDataConverter<Instance<
  typeof CompanyModel
>> = {
  toFirestore(modelObject: TCompanySnapshot): firebase.firestore.DocumentData {
    return modelObject;
  },
  fromFirestore(
    snapshot: firebase.firestore.QueryDocumentSnapshot<TCompanySnapshot>,
    options: firebase.firestore.SnapshotOptions
  ): Instance<typeof CompanyModel> {
    const data = snapshot.data(options);
    return CompanyModel.create({
      id: snapshot.id,
      ...data
    });
  }
};

The problem

Now, when I tried to create method that create new company, I'm getting typing error

function addCompany(
  // companyData: SnapshotIn<typeof CompanyModel>,
  companyData: TCompanySnapshot
): Promise<CompanyReference> {
  const added = firestore
    .collection("companies")
    // .withConverter<Instance<typeof CompanyModel>>(createCompanyConverter)
    .withConverter<TCompanySnapshot>(createCompanyConverter)
    .add(companyData);

  return added;   // <<<<<------ ERROR HERE
}

Property 'id' is missing in type

You can check this error on code sandbox

https://codesandbox.io/s/firebase-typescript-mobx-state-tree-n9s7o?file=/src/index.ts


Solution

  • The issue you are getting is caused by your use of withConverter(), which is changing the type of the CollectionReference so that it doesn't match your CompanyReference.

    See Firestore#collection(), CollectionReference#withConverter() and CollectionReference#add():

    firestore
      .collection("companies")                                 // CollectionReference<DocumentData>
      .withConverter<TCompanySnapshot>(createCompanyConverter) // CollectionReference<TCompanySnapshot>  
      .add(companyData);                                       // Promise<DocumentReference<TCompanySnapshot>>
    

    Note that DocumentReference<TCompanySnapshot> !== DocumentReference<Instance<typeof CompanyModel>>.

    If you are happy with how your object is being converted, you could forcibly override the type using the following line:

    return added as Promise<CompanyReference>;
    

    However, you should instead correct your FirestoreDataConverter instance. There is also no need to explicitly over-type everything as just specifying firebase.firestore.FirestoreDataConverter<Instance<typeof CompanyModel>> as the result's type should spread the appropriate types to the other objects.

    const createCompanyConverter: firebase.firestore.FirestoreDataConverter<Instance<typeof CompanyModel>> = {
      toFirestore(modelObject) {
        const objToUpload = { ...modelObject } as firebase.firestore.DocumentData; // DocumentData is mutable
        delete objToUpload.id; // make sure to remove ID so it's not uploaded to the document
        return objToUpload;
      },
      fromFirestore(snapshot, options) {
        const data = snapshot.data(options); // "as Omit<Instance<typeof CompanyModel>, "id">" could be added here
    
        // spread data first, so an incorrectly stored id gets overridden
        return CompanyModel.create({
          ...data, 
          id: snapshot.id
        });
      }
    };