How to customise firebase DocumentData interface? - node.js

I'm using firebase#5.5.8 and typescript#3.1.4
whenever I create a document from firestore i get an object of type firebase.firestore.DocumentReference as expected
If I call the get(options?: firebase.firestore.GetOptions|undefined) I'll get a object of type firebase.firestore.DocumentSnapshot as expected
If i call data(options?: firebase.firestore.DataOptions|undefined) I'll get either a firebase.firestore.DocumentData object or undefined, as expected
Now, on my manager object, I know what I'm writing to the databse, so I can make the assertion that whatever DocumentData you get out of my manager, you'll get a Client as shown
export interface Client {
name: string;
website?: string;
description?: string;
visible: boolean;
}
I want to create an interface for my manager object that expresses that. So I've tried:
export interface FirebaseDocumentSnapshot<T> extends $firebase.firestore.DocumentSnapshot {
data(options?: $firebase.firestore.SnapshotOptions | undefined): T|undefined
}
export interface FirebaseDocumentReference<T> extends $firebase.firestore.DocumentReference {
get(options?: $firebase.firestore.GetOptions | undefined): Promise<FirebaseDocumentSnapshot<T>>
}
The problem i've got is here:
const client: Client = mapClient(args);
const result: FirebaseDocumentReference<Client> = await $db.add(client);
the error is:
[ts]
Type 'DocumentReference' is not assignable to type 'FirebaseDocumentReference<Client>'.
Types of property 'get' are incompatible.
Type '(options?: GetOptions | undefined) => Promise<DocumentSnapshot>' is not assignable to type '(options?: GetOptions | undefined) => Promise<FirebaseDocumentSnapshot<Client>>'.
Type 'Promise<DocumentSnapshot>' is not assignable to type 'Promise<FirebaseDocumentSnapshot<Client>>'.
Type 'DocumentSnapshot' is not assignable to type 'FirebaseDocumentSnapshot<Client>'.
Types of property 'data' are incompatible.
Type '(options?: SnapshotOptions | undefined) => DocumentData | undefined' is not assignable to type '(options?: SnapshotOptions | undefined) => Client | undefined'.
Type 'DocumentData | undefined' is not assignable to type 'Client | undefined'.
Type 'DocumentData' is not assignable to type 'Client'.
Property 'name' is missing in type 'DocumentData'. [2322]
const result: FirebaseDocumentReference<Client>
How can i declare the interfaces so I can know the type of the resulting object?

A simple solution would be to cast the return data:
const client = snapshot.data() as Client

#gal-bracha's solution will "work" in the sense that the TypeScript compiler will assume client is of type Client, but it doesn't prevent a runtime error if bad data ended up in Firestore. A safer solution when dealing with any data external to your app is to use something like Yup to validate data explicitly:
import * as yup from "yup";
const clientSchema = yup.object({
name: yup.string(),
website: yup.string().url().notRequired(),
description: yup.string().notRequired(),
visible: yup.boolean()
});
// You can use this elsewhere in your app
export type Client = yup.InferType<typeof clientSchema>;
// Usage
const client = await clientSchema.validate(snapshot.data());
This is more defensive as clientSchema.validate will throw an error if, for some reason, bad data ended up in Firestore. After validate(), you're guaranteed, that client is a Client and not just telling the TypeScript compiler "treat this as a Client even though at runtime, it may not be".

export declare function getDocs<T>(query: Query<T>): Promise<QuerySnapshot<T>>;

Related

Incompatible types due to readonly

I have a object containing a middleware property but cant get the types to work. It tells me that the two middlewares are incompatible because one of them is readonly. Is there a way to solve this? - thanks :)
Example
type Middleware = (...args: unknown[]) => unknown;
type Router = { middleware?: Middleware[] };
const router = {
middleware: [() => null],
} as const satisfies Router;
Error
type Router = {
middleware?: Middleware[] | undefined;
}
Type '{ readonly middleware: readonly [() => null]; }' does not satisfy the expected type 'Router'.
Types of property 'middleware' are incompatible.
The type 'readonly [() => null]' is 'readonly' and cannot be assigned to the mutable type 'Middleware[]'.
A quick solution would be to remove the as const, which makes the object literals readonly:
const router = {
middleware: [() => null],
} satisfies Router;
Depending on your use case: you can also change the type Router by adding the utility type Readonly:
type Router = { middleware?: Readonly<Middleware[]> };
Though you are not able to call e.g. push on router.middleware.

Creating a cross-sdk wrapper for Firebase (Firestore, Cloud Storage, and more)

I'm currently trying to find an abstraction that can allow me to run Firebase products (mainly Firestore, Storage, and Analytics) regardless of the platform (React Native, React, Node.js). I have looked at the REST API but would like to use the SDKs for all the features that they offer.
// web
import firebase from 'firebase';
type WebFirestore = ReturnType<typeof firebase.firestore>;
// cloud
import * as admin from 'firebase-admin';
type CloudFirestore = ReturnType<typeof admin.firestore>;
// native
import { FirebaseFirestoreTypes } from '#react-native-firebase/firestore';
type NativeFirestore = FirebaseFirestoreTypes.Module;
const API = (firestore: WebFirestore | CloudFirestore | NativeFirestore) => {
firestore
.collection('foo')
.doc('foo')
.get()
.then((resp) => true);
}
I'm trying to create a TypeScript type that can enable me to do the same (at least that's what I think). The API, on the outset, is kept consistent across platforms for these products but my guess is that the return types are different. By that I mean, I can run this function on all platforms as long as the firestore object belongs to the SDK on that platform.
I was thinking of creating a class that takes a flag ('web', 'cloud', 'native') and then also take the firestore object in the constructor. I tried running the code below but TypeScript says the following:
(property) Promise<T>.then: (<TResult1 = FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>, TResult2 = never>(onfulfilled?: (value: FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>) | (<TResult1 = firebase.firestore.DocumentSnapshot<...>, TResult2 = never>(onfulfilled?: (value: firebase.firestore.DocumentSnapshot<...>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>) | (<TResult1 = FirebaseFirestoreTypes.DocumentSnapshot<...>, TResult2 = never>(onfulfilled?: (value: FirebaseFirestoreTypes.DocumentSnapshot<...>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>)
Attaches callbacks for the resolution and/or rejection of the Promise.
#param onfulfilled — The callback to execute when the Promise is resolved.
#param onrejected — The callback to execute when the Promise is rejected.
#returns — A Promise for the completion of which ever callback is executed.
This expression is not callable.
Each member of the union type '(<TResult1 = DocumentSnapshot<DocumentData>, TResult2 = never>(onfulfilled?: (value: DocumentSnapshot<DocumentData>) => TResult1 | PromiseLike<...>, onrejected?: (reason: any) => TResult2 | PromiseLike<...>) => Promise<...>) | (<TResult1 = DocumentSnapshot<...>, TResult2 = never>(onfulfilled?: (value: DocumentSnapsh...' has signatures, but none of those signatures are compatible with each other.ts(2349)
I'm rather new to TypeScript and was wondering if there is a way to make this work. All the types individually work but their union doesn't work. Is there a better way to think about this layer of abstraction in TypeScript? I intend to host this on the Github package registry and all the products to have access to the internal API as functions that are currently - firestore, cloud storage, cloud functions, some REST API calls.
Switching based on string flag is almost never the "right" way. You want to replace if conditions with a level of abstraction.
Adapter Pattern
You might want to read up on the Adapter Pattern, which is a generalized OOP approach to this sort of situation. Instead of one class with type property, you would have a separate wrapper class for each type of store instance. These classes would all have the same public API interface SharedFirestore, but internally they could call different methods on their this.firestore to get the results. When you want to use a firestore, you would just require the type SharedFirestore and you would know that you could interact with it the same regardless of which store type it is.
That sort of setup looks like:
interface SharedFirestore {
getDoc( collectionPath: string, documentPath: string ): Document;
}
class WebFirestore implements SharedFirestore {
private firestore: firebase.firestore.Firestore;
constructor( app?: firebase.app.App ) {
this.firestore = firebase.firestore(app);
}
getDoc( collectionPath: string, documentPath: string ): Document {
return this.firestore.collection(collectionPath).doc(documentPath);
}
}
class CloudFirestore implements SharedFirestore {
private firestore: FirebaseFirestore.Firestore;
constructor( app?: admin.app.App ) {
this.firestore = admin.firestore(app);
}
getDoc( collectionPath: string, documentPath: string ): Document {
return this.firestore.someOtherMethod( collectionPath, documentPath );
}
}
Typescript Generics
Wrapper classes are not necessary here because the three types already implement the same interface, kind of. They all allow you to get a document by calling firestore.collection(collectionPath).doc(documentPath).get(). This is purely a typescript issue which is caused by the differing return types.
web.collection('foo').doc('foo').get();
// type: firebase.firestore.DocumentSnapshot<firebase.firestore.DocumentData>
cloud.collection('foo').doc('foo').get();
// type: FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>
native.collection('foo').doc('foo').get();
// type: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
Your then callback is a function of the document, but you don't know which of the three types of document you have. So you cannot call a function on the union. Instead we need to say "whichever type of store I have, my callback will match that". To do that, we make the API a generic which depends on the store type.
We can use some conditional types to extract the associated types for the collection and the document from the store type.
interface BaseCollection<D> {
doc(path: string): D;
}
interface BaseStore<C extends BaseCollection<any>> {
collection(path: string): C;
}
type CollectionFromStore<S> = S extends BaseStore<infer C> ? C : never;
type DocFromCollection<C> = C extends BaseCollection<infer D> ? D : never;
type DocFromStore<S> = DocFromCollection<CollectionFromStore<S>>
Here's a possible setup that uses generics to extend a base type rather than extending a union.
class FirebaseAPI <S extends BaseStore<any>> {
constructor( private firebase: S ) {}
getCollection( collectionPath: string ): CollectionFromStore<S> {
return this.firebase.collection(collectionPath);
}
getDoc( collectionPath: string, documentPath: string ): DocFromStore<S> {
return this.getCollection(collectionPath).doc(documentPath);
}
}
You can see how we get the appropriate return types.
(new FirebaseAPI(web)).getDoc('', '').get().then(v => {});
// v has type firebase.firestore.DocumentSnapshot<firebase.firestore.DocumentData>
(new FirebaseAPI(cloud)).getDoc('', '').get().then(v => {});
// v has type FirebaseFirestore.DocumentSnapshot<FirebaseFirestore.DocumentData>
(new FirebaseAPI(native)).getDoc('', '').get().then(v => {});
// v has type FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
Playground Link

Return type is not assignable

A 3rd party package has a function that looks like this...
function asKey(key: KeyObject | KeyInput, parameters?: KeyParameters): RSAKey | ECKey | OKPKey | OctKey;
All of the return types (RSAKey | ECKey | OKPKey | OctKey) are interfaces that extend Key.
I am using the asKey function and trying to set a return type of RSAKey because I know it only ever returns this 1 interface...
private foo = (): RSAKey => {
return asKey('foo');
};
However this fails with the following:
Type 'RSAKey | ECKey | OKPKey | OctKey' is not assignable to type 'RSAKey'.
Type 'ECKey' is not assignable to type 'RSAKey'.
Types of property 'kty' are incompatible.
Type '"EC"' is not assignable to type '"RSA"'.ts(2322)
If I change my return type to Key in my foo function I get no errors, but thats not what I want because RSAKey has an extra function I want to call that is not in Key. How can I best get this return type working?
When you are writing the following code
private foo = (): RSAKey => {
return asKey('foo');
};
Typescript is looking at asKey and see that there could be multiple different type that can be returned, and foo doesn't handle them, that's why you get an error.
If you know that askey will returns only a RSAKey type, you have to tell typescript:
It's ok, I know what's going on. Trust me, the type is RSAKey
And you do that using the keyword as
private foo = (): RSAKey => {
return asKey('foo') as RSAKey;
};

eslint-plugin-import how to add flow declared modules to exceptions

I have a file in flow-typed directory with some common type declarations like:
common-types.js
// #flow
declare module 'common-types' {
declare export type RequestState = {
setLoading: () => void,
setFinished: () => void,
setError: (error: AxiosFailure) => void,
reset: () => void,
status: LoadingStatus,
error: AxiosFailure | void,
};
declare export type LoadingStatus = '' | 'loading' | 'finished';
declare export type ErrorObject = { [key: string]: string | string[] | Error };
declare export type AxiosFailure = {
status: number,
data: ErrorObject,
}
}
Now I import it like this in files:
import type { RequestState } from 'common-types';
but I get eslint-plugin-import errors about missing file extension as well as unable to resolve path to module 'common-types'
How do I deal with it?
I found a solution. As #logansmyth suggested in comment
Your types should just pass your code along with your data
The problem I had was with webpack aliases. Flow pointed me errors about modules not being installed. Hovewer I found out that I can use mappers in .flowconfig like:
module.name_mapper='^common' ->'<PROJECT_ROOT>/src/common'
along with webpack aliases, which makes eslint-plugin-import and flow happy as well properly type-checking. Now I import types along with common components, no messing with flow-typed directory.

TypeScript warning => TS7017: Index signature of object type implicitly has any type

I am getting the following TypeScript warning -
Index signature of object type implicitly has any type
Here is the code that cases the warning:
Object.keys(events).forEach(function (k: string) {
const ev: ISumanEvent = events[k]; // warning is for this line!!
const toStr = String(ev);
assert(ev.explanation.length > 20, ' => (collapsed).');
if (toStr !== k) {
throw new Error(' => (collapsed).');
}
});
can anyone determine from this code block why the warning shows up? I cannot figure it out.
If it helps this is the definition for ISumanEvent:
interface ISumanEvent extends String {
explanation: string,
toString: TSumanToString
}
You could add an indexer property to your interface definition:
interface ISumanEvent extends String {
explanation: string,
toString: TSumanToString,
[key: string]: string|TSumanToString|ISumanEvent;
}
which will allow you to access it by index as you do: events[k];. Also with union indexer it's better to let the compiler infer the type instead of explicitly defining it:
const ev = events[k];

Resources