Typesaurus

repository·main·Indexed 19 days ago

https://github.com/kossnocorp/typesaurus

A TypeScript-first Object Document Mapper (ODM) for Google Cloud Firestore version 10.7.0. It provides end-to-end type safety, automatic conversion of Firestore types to native JavaScript types, and support for both browser (via firebase) and Node.js (via firebase-admin) environments. Key features include a centralized schema, typed IDs, and a structured approach to queries and atomic updates using queryHelpers and writeHelpers.

Tokens
12.3K
Snippets
29
Records
60
Agent score
66%

What's inside typesaurus

  1. Overview of Typesaurus features

    main

    Typesaurus is a TypeScript-first Object Document Mapper (ODM) for Firestore designed for uncompromised type-safety and developer experience.

    Key features include:

    • Complete type-safety: Handles Firestore-specific quirks while maintaining strict types.
    • Universal code: The same codebase can be used in both browser and Node.js environments.
    • JavaScript-native types: Automatically converts Firestore data types (like Timestamp) into native JavaScript types (like Date).
    • Typed IDs: Document IDs are treated as distinct types, preventing logic errors like mixing up a UserId with an AccountId.
    • Centralized schema: Provides a single location to define, read, and update your data models.
    • Single-import principle: Designed so you can define your schema in one place and import it where needed.
  2. Install Typesaurus

    main

    To use Typesaurus, you must install it along with the appropriate Firebase SDK for your environment. Typesaurus does not include these as automatic dependencies.

    • Web/Browser environment: Requires firebase.
    • Node.js/Server environment: Requires firebase-admin.

    To install everything needed for a project, run:

    npm install --save typesaurus firebase firebase-admin
  3. Access subcollections via the `sub` property

    main

    In Typesaurus, collections can be nested. If a collection contains subcollections, you can access them through the .sub property. This maintains the hierarchical type safety of your schema.

    For example, if your schema defines comments inside posts, you access it via db.posts.sub.comments.

    // Accessing a subcollection
    const commentId = db.posts.sub.comments.id('some-id');
  4. How the batch API structure works

    main

    The batch API creates a specialized version of your database schema designed for staging changes.

    1. RootDB: The object returned by batch(). It behaves like your database schema but is also a Promise<void>. Calling it as a function triggers the atomic commit.
    2. BatchDB: A recursive type that maps your database structure. If a path in your DB is a NestedCollection, it provides a NestedCollection interface; if it is a standard Collection, it provides a Collection interface.
    3. NestedCollection: For nested structures, a NestedCollection allows you to access a specific document by its ID via a function call (e.g., collection(id)), which then returns a schema scoped to that document's sub-collections.
    4. Collection: Provides the standard write methods (set, upset, update, remove) and a path string representing the Firestore path.
  5. Understand document data and server dates

    main

    Typesaurus handles the difference between client and server environments regarding ServerDate fields.

    • Server Environment: ServerDate fields resolve to Date objects.
    • Client Environment: Depending on your dateStrategy, ServerDate fields might resolve to Date | null (if using estimate or previous and the date is missing from the payload).

    DocData is the type representing the shape of data returned from the database, automatically resolving these date types based on the RuntimeEnvironment ('server' | 'client') and DataSource ('cache' | 'database').

  6. Handle server dates with ServerDate and NormalizeServerDates

    main

    Typesaurus provides specialized types for managing server-side timestamps:

    • Typesaurus.ServerDate: Use this to define a field that should be set to the server's current date upon document creation.
    • Typesaurus.NormalizeServerDates<Type>: A utility type that deeply replaces all ServerDate markers in a given type with regular JavaScript Date objects. This is useful when passing data to non-Typesaurus environments or when storing server dates in arrays (where ServerDate is not allowed).
  7. Subscribe to real-time updates

    main

    Typesaurus uses SubscriptionPromise to handle real-time data via Firestore snapshots. Both get() and query() return an object that can be used to either fetch data once or subscribe to changes.

    Usage:

    • .get(): Returns a Promise that resolves with the current data.
    • .subscribe(onResult, onError): Registers callbacks that trigger whenever the underlying data changes.

    Subscription Result Metadata: When subscribing to a collection (via all()) or a query, the onResult callback receives the data and a metadata object containing:

    • size: Number of documents in the snapshot.
    • empty: Boolean indicating if the snapshot is empty.
    • changes(): A function that returns an array of document changes (type, oldIndex, newIndex, doc).
  8. Narrow types using 'As' casting functions

    main

    Typesaurus provides several As interfaces (CollectionAs, GroupAs, RefAs, and DocAs) that act as type-safe casting functions. These allow you to narrow a generic entity to a specific Model type.

    If the provided Model matches the underlying definition, the function returns the corresponding shared type (Collection<Model>, Group<Model>, etc.). If the models do not match, the result resolves to unknown.

    • CollectionAs<Def>: Narrows to Collection<Model>.
    • GroupAs<Def>: Narrows to Group<Model>.
    • RefAs<Def>: Narrows to Ref<Model>.
    • DocAs<Def, Props>: Narrows to Doc<Model, Props>.
  9. Filter data using query helpers

    main

    Typesaurus provides a fluent API for building filters via the Helpers object (usually passed as $ in a query function).

    Field Selection

    • field(key): Selects a top-level field.
    • field(key1, key2, ...): Supports deep nested field selection up to 10 levels deep.
    • docId(): Selects the document's unique identifier.

    Comparison Operators

    Once a field is selected, you can apply filters using:

    • eq(value): Equals
    • not(value): Not equals
    • lt(value) / lte(value): Less than / Less than or equal
    • gt(value) / gte(value): Greater than / Greater than or equal
    • in(values[]): Value is in the provided array
    • notIn(values[]): Value is not in the provided array

    Array Operators

    If the selected field is an array, you can use:

    • contains(value): Checks if the array contains the specific value.
    • containsAny(values[]): Checks if the array contains any of the values in the provided array.
  10. Use `as` to narrow a collection to a shared type

    main
    The as property allows you to narrow a collection type to a Shared.CollectionAs type. Unlike a regular collection, a shared collection lacks certain methods like set, upset, and as because type safety for those operations depends on knowing the full model type. The ref property is also limited in a shared collection. If models do not match during narrowing, it resolves to unknown.
  11. How Collections, Docs, and Refs work together

    main

    Typesaurus uses three primary abstractions to interact with data:

    1. Collection: Represents a group of documents. Used for querying, adding new documents, or performing aggregate operations (like count, sum, average).
    2. Doc: Represents a single document retrieved from the database. It contains the document data and a ref to itself.
    3. Ref: A pointer to a specific document. It is used to perform updates, deletes, or set/upset operations on a document without needing the full document data locally.

    Lifecycle Example:

    • Use collection.add(data) to create a new document and get a Ref.
    • Use collection.get(id) to get a Doc.
    • Use doc.update(data) or ref.update(data) to modify existing data.
  12. Understand the Update Argument types

    main

    When calling an update method, the Arg type determines what you can pass. It is a union of several possibilities:

    • ArgData: The actual data payload. This includes Core.WriteData (for special values like increments or server dates), MinimalData (for reduced models), or standard Data (partial model objects).
    • ArgGetter: A function ($: Helpers) => Data that allows you to compute the update values dynamically using the provided helpers.
    • UpdateField: A specific object structure { key: string | string[], value: any } used for targeted updates.