🔥High Precision Typescript Wrapper for Firestore Web, Providing Unparalleled Type Safe and Dev Experience
MIT License
FirelordJS is the only library capable of providing truly generic type safety while exposing almost all the API of the official Firestore SDK. The goal is to end Firestore typing madness.
Example of how Firelord transforms ServerTimestamp
type in different operations:
FirelordJS:
Record<string,Record<string,Record<string,...>>>
, max 1000 levels.a
, a.b
, a.b.c
, a.b.d
, a.x
, a.x.y
, a.x.z
) with type safety.Firelord is the only library capable of typing against Firestore limitations. It also blocks undocumented errors and prevent pesky runtime errors like empty array errors filter, cursors and implicit data deletion in update operation. Firelord provides over 30 custom error messages to assist you in writing proper Firestore code! Here is an example:
I am confident Firelord is the best among its kind in terms of best safety and developer experience. I stand behind my words, and I will buy you x cups of coffee if you:
The bounty has been available and keeps increasing since July 14, 2022.
(Web feature only, admin can skip this section)
Rulings for or
& and
composite query are ready, rulings works with nested query, example:
Official SDK runtime error:
Firelord compile time error:
It has all the regular rulings plus new composite rulings. See also peeling composite query error messages
Below ESLint rules give false error, please turn them off.
{
"rules": {
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-redundant-type-constituents": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
}
}
(Web version's issue only, admin can skip this section)
FirelordJS built files are a bit complicated. It is not an ESM module(no "type":"module"
in package.json
) but it also does not transpile import
statements to CommonJS.
Results tested with various build tools:
import
statements (Pseudo ESM)create-react-app
@vitejs/plugin-react-swc
@sveltejs/vite-plugin-svelte
nextjs
with next-transpile-modules
@sveltejs/kit/vite
@vitejs/plugin-react-swc
@sveltejs/vite-plugin-svelte
nextjs
with next-transpile-modules
create-react-app
, see issue
@sveltejs/kit/vite
see issue
create-react-app
@vitejs/plugin-react-swc
@sveltejs/vite-plugin-svelte
nextjs
.@sveltejs/kit/vite
Using CommonJS Firelord with Firebase v9.17.0 and beyond break most of the build tools because of this Firebase issue
Pseudo ESM has the highest compatibility which is why it is the default package.
If you see cannot use import statement outside a module
error, please install the CommonJS version with
npm i firelordjs@cjs
These are types that are supported but be careful when using them. They are not limitations, but how things work.
By design Firelord banned mapped type, this was until version 2.5.10. To understand why mapped was banned in the first place and why it is possible now, see this issue. In short, querying mapped type requires extra information, make sure you know what you are doing.
Object unions type was banned before v2.6.2 because it brings uncertainty that cannot be handled reasonably. For example, with {a:number}|{b:string}
, you can set {a:1}
then update {b:"x"}
, in this case the type is no longer unions type but an intersection type: {a:number} & {b:string}
. This limitation is lifted to allow users to fully utilize discriminated unions. Plus in future update operation Mandatory Field could mitigate this problem.
Mandatory field update. Example, for field like updatedAt
, it is mandatory to includes it every time you update the document. There are two ways to implement these feature: via Meta Type and via abstraction. With Meta Type(using special field value), it is less flexible because we no longer able to exclude it from all update operations. With abstraction, it is more flexible but require more works from user. I prefer via abstraction due to it does not shut down other use cases despite having lower user experience.
Support tuple data type.
Replace set merge
with upset
(update if exists, else set). It will receive 1 doc ref argument and 2 data arguments(partial data and complete data). It will attempt to update the document with partial data or create a document with complete data if the document does not exist.
More in code documentation and tests.
Add support for V2 Firestore function triggers(for Firelord admin version only).
Support for optional (?
modifier). Optional is a highly requested feature because of how common it is, however because of how Firestore works: it is impossible to query a missing field. Example: it is impossible to query user that has no phone number if phone number field does not exist. Because of this, it is important to make sure every field exists. You may not need the field now, but you may need it later plus adding default value is simple, especially with such powerful typing library like Firelord. So in order to not accidentally cripple your query in the future, I will not implement this feature. Yes, set merge basically lead to the same problem, hence I encourage you to use upset
instead (will be available in the future).
Narrow read type base on query constraint. For example where('a', '==', true)
will narrow the read type of field a
to true
, it should be able to narrow down complex case like where('a.b.c', '==', { d:[{e:1}] })
. Expected to support ==
comparator for all types and possibly !=
comparator for literal type(type filtering for!=
comparator poses great complexity hence I may not work on it). Update: I decided to give this up because with the introduction of composite query, it will be extremely difficult to implement this. Plus unlike narrowing down write type, narrowing down the read type does not contribute to type safety, it just makes thing slightly simpler(skip exhaustive check).
You can use at most one array-contains or array-contains-any clause per query. You can't combine array-contains with array-contains-any
is not enabled, see this release note
or()
/ and()
) value is wrong if the field is __name__
of collection reference, example: query(collectionRef, or(where("__name__", "==", "id_only_not_full_path")))
result in false negative because Firelord will ask for full path but we only need full path if the reference is group collection.