Skip to content

Instantly share code, notes, and snippets.

@Jeandcc
Last active February 27, 2022 16:56
Show Gist options
  • Save Jeandcc/dabf96fba466d98381cac5512d07f4b0 to your computer and use it in GitHub Desktop.
Save Jeandcc/dabf96fba466d98381cac5512d07f4b0 to your computer and use it in GitHub Desktop.
Support for type-safe queries and updates directly from converters. This removes the need of manually doing it for every call to "update" or when performing queries with "where".
import { firestore } from "firebase-admin";
/* START - Unchanged */
type PathImpl<T, K extends keyof T> = K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}`
: K | `${K}.${PathImpl<T[K], keyof T[K]>}`
: K
: never;
type Path<T> = PathImpl<T, keyof T> | keyof T;
type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends Path<T[K]>
? PathValue<T[K], Rest>
: never
: never
: P extends keyof T
? T[P]
: never;
/* END - Unchanged */
type TFieldsInDotNotation<T> = Path<T>; // CHANGE: Renamed
type TUpdateData<T> = Partial<
{
[TKey in TFieldsInDotNotation<T>]:
| PathValue<T, TKey>
| firestore.FieldValue; // CHANGE: Added support for Firestore fields (e.g. FieldValue.increment(), FieldValue.arrayUnion(), etc.)
}
>;
// Extracted from firestore.
// For some reason couldn't import directly from there
type WhereFilterOp =
| "<"
| "<="
| "=="
| "!="
| ">="
| ">"
| "array-contains"
| "in"
| "not-in"
| "array-contains-any";
/**
* Down below, we omit a series of properties from types exported from
* firestore. We do that so we can implement our own types to those
* properties. There likely is a better approach to this, but this
* works for now.
*/
interface TCustomQuery<T> extends Omit<firestore.Query<T>, "where"> {
where(
fieldPath: TFieldsInDotNotation<T>, // TODO: Improve for accessing optional fields. (Optional fields not coming up as valid queries)
opStr: WhereFilterOp,
value: any
): TCustomQuery<T>;
}
type TFirestoreCollectionRef<T> = Omit<
firestore.CollectionReference<T>,
"doc" | "where" // Fields that will be overridden
> &
TCustomQuery<T>;
interface TCustomDocRef<T>
extends Omit<firestore.DocumentReference<T>, "update"> {
update(data: TUpdateData<T>): Promise<firestore.WriteResult>; // Omitted "update" so we could override it
}
interface TCustomCollectionRef<T> extends TFirestoreCollectionRef<T> {
doc(documentPath: string): TCustomDocRef<T>; // Override doc() to match our custom doc() type
doc(): TCustomDocRef<T>;
}
const converter = <T>() => ({
toFirestore: (data: Partial<T>) => data,
fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => {
return { ...snap.data(), id: snap.id } as T & { id: string }; // Adds id to data
},
});
export const collectionConverter = <T>(collectionPath: string) =>
firestore()
.collection(collectionPath)
.withConverter(converter<T>()) as TCustomCollectionRef<T>; // Override type of Collection to be the custom one we created
// USAGE
const db = { pilots: collectionConverter<IPilot>("pilots") };
@rscotten
Copy link

rscotten commented Feb 27, 2022

@Jeandcc this is really nice. I started implementing TUpdateData and noticed that it can't handle nested computed keys.

For example:

type User = {
  name: string;
  email: string;
  favorites: Record<
    string,
    {
      id: string;
      updatedAt: number;
    }
  >;
};

const a = 'asdf';

const partialUser: TUpdateData<User> = {
  [`favorites.${a}.updatedAt`]: Date.now(),
};

Any idea why this is?
Using TS v4.5.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment