@info-lounge/firestore-typed
v0.7.0
Published
Type-safe, low-level wrapper for Firebase Firestore with enhanced validation and developer experience
Downloads
699
Readme
@info-lounge/firestore-typed – Type-safe Firestore wrapper with runtime validation
A type-safe, low-level wrapper for Firebase Firestore with a validator-first design. Every collection requires a runtime validator (typia or zod): writes are validated by default, and reads are validated too when validateOnRead is enabled, providing comprehensive type safety, data integrity, and improved developer experience for Firestore operations.
Quick Example - Why Use FirestoreTyped?
// ❌ Raw Firestore - No validation, runtime errors possible
const db = getFirestore()
await db.collection('users').doc('123').set({
name: 123, // Wrong type but no error until runtime
email: null // Missing required field
})
// ✅ FirestoreTyped - Automatic validation prevents errors
import { getFirestoreTyped } from '@info-lounge/firestore-typed'
import typia from 'typia'
interface User {
name: string
email: string
}
const userValidator = typia.createAssert<User>()
const db = getFirestoreTyped()
const users = db.collection<User>('users', userValidator)
await users.doc('123').set({
name: 'John', // ✓ Validated at runtime
email: '[email protected]' // ✓ All fields checked
})
// Throws validation error if data doesn't match User interface⚠️ EXPERIMENTAL RELEASE: This package is currently in beta. APIs may change in future releases. Use with caution in production environments.
Key Features
- 🛡️ Validator-First Architecture: every collection requires a runtime validator — writes are validated by default, reads opt in via
validateOnRead: true - 🔒 Type Safety: Full TypeScript compile-time and runtime type checking
- ⚡ Performance Optimized: Minimal overhead with maximum data integrity
- 🎯 Firebase Native: Direct mapping to Firestore's native API patterns
- 🔧 Configurable: Flexible validation settings per operation or globally
Table of Contents
- Installation
- Quick Start
- Core Architecture
- Configuration
- CRUD Operations
- Query Builder
- Validation
- Automatic Type Conversion
- Type-Safe Document References
- Error Handling
- Performance
- Best Practices
- Advanced Usage
- API Reference
Installation
npm install @info-lounge/firestore-typedRequirements
- Node.js: >= 22
- firebase-admin:
^13.5.0 || ^14.0.0(peer dependency) - Validation library (peer dependency, choose at least one):
- typia:
>=9.7.2 <13(v13 requires TypeScript 7 + ttsc, which is not supported yet) - zod:
^4.0.17
- typia:
Dependencies
This package requires the following peer dependencies:
# Required
npm install firebase-admin
# Choose one validation library (or both):
npm install typia # Recommended for best performance
# OR
npm install zod # Alternative validation libraryNote: You must install either typia or zod (or both) as a validation library. The package will work with either one.
Typia Transform Configuration
Important: Typia requires TypeScript transform plugin configuration to work properly. Use ts-patch for the easiest setup.
1. Install ts-patch
npm install -D ts-patch2. Patch TypeScript compiler
npx ts-patch install3. Configure tsconfig.json
{
"compilerOptions": {
"strict": true,
"plugins": [
{
"transform": "typia/lib/transform"
}
]
}
}4. Use patched TypeScript compiler
# Instead of tsc
npx tsc
# For ts-node
npx ts-node your-file.tsNote: Without proper transformer configuration,
typia.createAssert<T>()will not generate validation code and may throw runtime errors. The ts-patch approach is the most reliable method for most projects.
Quick Start
Why FirestoreTyped?
FirestoreTyped's core principle: no collection without a validator. Unlike raw Firestore operations, every write is validated by default, and reads are validated as well when validateOnRead is enabled (off by default for performance).
// ❌ Raw Firestore - No validation, potential runtime errors
const db = getFirestore()
await db.collection('users').doc('123').set({
name: 123, // Wrong type, but no error until runtime
email: null // Missing required field
})
// ✅ FirestoreTyped - Automatic validation prevents errors
const db = getFirestoreTyped()
const users = db.collection<UserEntity>('users', userValidator)
await users.doc('123').set({
name: 'John', // Validated at runtime
email: '[email protected]' // All fields checked
})
// Throws validation error if data doesn't match UserEntityBasic Usage
import { getFirestoreTyped } from '@info-lounge/firestore-typed'
import typia from 'typia'
// 1. Define your data structure
interface UserEntity {
name: string
email: string
createdAt: Date
updatedAt: Date
}
// 2. Create validator (REQUIRED - this is what makes data safe)
const userEntityValidator = typia.createAssert<UserEntity>()
// 3. Create FirestoreTyped instance
const db = getFirestoreTyped(undefined, {
validateOnWrite: true, // Default: validates all writes
validateOnRead: false // Optional: validates all reads
})
// 4. Create typed collection with validator
const usersCollection = db.collection<UserEntity>('users', userEntityValidator)
// ✅ This data will be validated before write
await usersCollection.doc('user-001').set({
name: 'John Doe',
email: '[email protected]',
createdAt: new Date(),
updatedAt: new Date()
})
// ✅ This data will be validated after read (if validateOnRead: true)
const user = await usersCollection.doc('user-001').get()
// With validateOnRead: true, user.data is guaranteed to match UserEntity
// (or a validation error is thrown); without it, data is returned as-isUsing Custom Firestore Instances
When you need to use a different Firestore instance (e.g., different project, different database), you can pass a custom Firestore instance:
import { getFirestore } from 'firebase-admin/firestore'
import { initializeApp, getApps } from 'firebase-admin/app'
import { getFirestoreTyped } from '@info-lounge/firestore-typed'
// Initialize custom Firebase app
const customApp = getApps().find(app => app.name === 'custom-app') ||
initializeApp({
projectId: 'custom-project-id',
// other configuration...
}, 'custom-app')
// Get custom Firestore instance
const customFirestore = getFirestore(customApp)
// Initialize FirestoreTyped with custom Firestore instance
const customDb = getFirestoreTyped(customFirestore, {
validateOnWrite: true,
validateOnRead: false
})
// Use as normal
const usersCollection = customDb.collection<UserEntity>('users', userValidator)
await usersCollection.doc('user-001').set(userData)
// Using named databases
const namedDatabase = getFirestore(customApp, '(named-database)')
const namedDb = getFirestoreTyped(namedDatabase, {
validateOnWrite: true
})Use Cases:
- Multi-project: Working with multiple Firebase projects
- Environment separation: Different databases for development, staging, production
- Named databases: Using Firestore's multiple database feature
- Testing: Using dedicated Firestore emulator instances
Complete CRUD Example
// Define user data structure
interface User {
name: string
email: string
createdAt: Date
updatedAt: Date
}
// Create validator function
const userValidator = typia.createAssert<User>()
// Initialize FirestoreTyped
const db = getFirestoreTyped(undefined, {
validateOnRead: true,
validateOnWrite: true
})
const usersCollection = db.collection<User>('users', userValidator)
// CREATE: Add document with auto-generated ID
const newUser = {
name: 'John Doe',
email: '[email protected]',
createdAt: new Date(),
updatedAt: new Date()
}
const docRef = await usersCollection.add(newUser)
console.log('Created user with ID:', docRef.id)
// CREATE: Set document with specific ID
const specificUser = {
name: 'Jane Smith',
email: '[email protected]',
createdAt: new Date(),
updatedAt: new Date()
}
await usersCollection.doc('user-123').set(specificUser)
// READ: Get single document
const userSnapshot = await usersCollection.doc('user-123').get()
if (userSnapshot.metadata.exists) {
const user = userSnapshot.data!
console.log(`User: ${user.name} (${user.email})`)
}
// READ: Get all documents
const allUsers = await usersCollection.get()
const userList = allUsers.docs.map(doc => ({
id: doc.metadata.id, // Use Firestore document ID
...doc.data!
}))
// UPDATE: Merge partial data
await usersCollection.doc('user-123').merge({
email: '[email protected]',
updatedAt: new Date()
})
// UPDATE: Complete replacement
const updatedUser: User = {
name: 'Jane Smith',
email: '[email protected]',
createdAt: new Date('2024-01-01'),
updatedAt: new Date()
}
await usersCollection.doc('user-123').set(updatedUser)
// DELETE: Remove document
await usersCollection.doc('user-123').delete()Core Architecture
FirestoreTyped Instance
FirestoreTyped provides a low-level, type-safe interface to Firestore with built-in validation. The main entry point is the getFirestoreTyped() factory function:
const db = getFirestoreTyped(firestore?: Firestore, options?: FirestoreTypedOptions)Architecture Philosophy
FirestoreTyped follows a low-level, collection-specific validation approach:
- Type Safety: Generic types ensure compile-time safety
- Flexible Validation: Collection-level validators for different entity types
- Firebase Native: Direct mapping to Firestore's native API patterns
- Performance Focused: Minimal overhead with maximum type safety
Collection Reference
Collections are accessed through the collection() method with type-specific validator:
// Validator is provided per collection for maximum flexibility
const collection = db.collection<T>(path: string, validator: (data: unknown) => T)Document Reference
Documents are accessed through the doc() method on collections:
const doc = collection.doc(id: string)Document Snapshot
Read operations return a DocumentSnapshot<T> with type-safe data access:
const snapshot = await doc.get()
const data = snapshot.data // Type-safe data access with automatic validation
const exists = snapshot.metadata.existsConfiguration
Global Options
interface FirestoreTypedOptions {
validateOnRead?: boolean // Default: false
validateOnWrite?: boolean // Default: true
}Validator Configuration
FirestoreTyped uses collection-level validators for flexible entity type support:
// Using typia for compile-time type generation
import typia from 'typia'
interface UserEntity {
name: string
email: string
createdAt: Date
updatedAt: Date
}
// Create validator function
const userValidator = typia.createAssert<UserEntity>()
// Initialize FirestoreTyped
const db = getFirestoreTyped(undefined, {
validateOnRead: true,
validateOnWrite: true
})
// Create typed collection with validator
const usersCollection = db.collection<UserEntity>('users', userValidator)Operation Options
interface ReadOptions {
validateOnRead?: boolean // Override global setting
}
interface WriteOptions {
validateOnWrite?: boolean // Override global setting
failIfExists?: boolean // Prevent overwriting existing documents
}CRUD Operations
All CRUD operations require a typed collection created with a validator:
// Setup: Create typed collection
const db = getFirestoreTyped()
const collection = db.collection<UserEntity>('users', userValidator)Create Documents
// Add with auto-generated ID
await collection.add(data)
// Set with specific ID
await collection.doc('specific-id').set(data)
// Set with write options
await collection.doc('id').set(data, { validateOnWrite: false })
// Ensure document creation without overwriting
await collection.doc('id').set(data, { failIfExists: true })
// Throws DocumentAlreadyExistsError if document already exists
// (atomic — enforced server-side via Firestore's create(), safe under concurrency)Read Documents
// Get single document
const snapshot = await collection.doc('id').get()
const data = snapshot.data
// Get single document with read options
const snapshot = await collection.doc('id').get({ validateOnRead: true })
// Get all documents in collection
const querySnapshot = await collection.get()
const documents = querySnapshot.docs.map(doc => doc.data)Merge Partial Data
Important:
- FirestoreTyped uses a dedicated
merge()method instead ofset(data, { merge: true }) - The
mergeoperation validates the complete merged data, not just the partial data being merged - The document must exist - throws
DocumentNotFoundErrorif the document doesn't exist - The read-merge-validate-write sequence runs inside a transaction: a concurrent update between the read and the write causes the merge to retry against fresh data instead of overwriting it
- Native Firestore's
set(..., { merge: true })is not available in FirestoreTyped
// Partial merge with existing data
// 1. Retrieves existing document data
// 2. Merges with new partial data
// 3. Validates the COMPLETE merged result against the entity type
await collection.doc('id').merge({ field: 'newValue' })
// Example with validation
interface User {
name: string
email: string
age: number
}
// Existing document: { name: 'John', email: '[email protected]', age: 30 }
// Merge operation:
await usersCollection.doc('user123').merge({
email: '[email protected]'
})
// Result validated: { name: 'John', email: '[email protected]', age: 30 }
// ✅ Passes validation as complete User entity
// ❌ This would fail validation:
// await usersCollection.doc('user123').merge({ invalidField: 'value' })
// Because merged result would have extra field not in User interface
// Merge with validation options
await collection.doc('id').merge(data, { validateOnWrite: true })
// ❌ NOT available in FirestoreTyped (use merge() instead):
// await collection.doc('id').set(data, { merge: true }) // This pattern is NOT supported
// ✅ Use dedicated merge() method:
await collection.doc('id').merge(data) // This is the correct FirestoreTyped way
// Handle non-existent document error
try {
await collection.doc('non-existent').merge({ field: 'value' })
} catch (error) {
if (error instanceof DocumentNotFoundError) {
console.log('Document does not exist:', error.documentPath)
// Create the document instead
await collection.doc('non-existent').set(fullData)
}
}Delete Documents
await collection.doc('id').delete()Query Builder
FirestoreTyped provides a complete type-safe query builder that allows you to use Firebase Firestore's powerful query capabilities with full type safety.
Basic Queries
// Setup: Create typed collection
const db = getFirestoreTyped()
const usersCollection = db.collection<UserEntity>('users', userValidator)
// Single condition search
const johnUsers = await usersCollection
.where('name', '==', 'John Doe')
.get()
// Sorting functionality
const sortedUsers = await usersCollection
.orderBy('createdAt', 'desc')
.get()
// Result limiting
const latestUsers = await usersCollection
.orderBy('updatedAt', 'desc')
.limit(10)
.get()Complex Queries and Method Chaining
// Multiple condition combined query
const filteredUsers = await usersCollection
.where('email', '>=', '[email protected]')
.where('email', '<=', '[email protected]')
.orderBy('name', 'asc')
.limit(5)
.get()
// Product type-based search
const cellProducts = await productsCollection
.where('category', '==', 'electronics')
.where('price', '<=', 1000)
.orderBy('name')
.get()Advanced Pagination
// Cursor-based pagination
const firstPage = await usersCollection
.orderBy('createdAt', 'desc')
.limit(20)
.get()
if (firstPage.docs.length > 0) {
const lastDoc = firstPage.docs[firstPage.docs.length - 1]
// Get next page
const secondPage = await usersCollection
.orderBy('createdAt', 'desc')
.startAfter(lastDoc.data!.createdAt)
.limit(20)
.get()
}
// Range queries
const rangeQuery = await usersCollection
.orderBy('name')
.startAt('Alice')
.endAt('John')
.get()Type Safety
The query builder provides type safety for field names and basic operations:
// ✅ Valid usage - field names are type-checked
const validQuery = usersCollection.where('name', '==', 'John Doe')
const sortedQuery = usersCollection.orderBy('createdAt', 'desc')
// ❌ Compile errors for invalid field names
// const invalidQuery = usersCollection.where('invalidField', '==', 'value')
// ✅ Operand types are checked per operator
// usersCollection.where('name', '==', 123) // ❌ number is not assignable to string
// usersCollection.where('name', '==', undefined) // ❌ undefined is never a valid operand
// usersCollection.where('status', 'in', ['active', 'inactive']) // ✅ 'in' takes an array
// usersCollection.where('tags', 'array-contains', 'admin') // ✅ element type of the array field
// ✅ Native Firestore values are accepted alongside serialized forms
// usersCollection.where('createdAt', '>=', Timestamp.fromDate(date)) // Date fields also take Timestamp
// ⚠️ Note: pagination cursor parameters are not type-checked
// const paginated = usersCollection.startAt('any', 'values') // Parameters are unknown[]Validation Integration
// Query results automatically validated based on global configuration
const validatedResults = await usersCollection
.where('name', '!=', '')
.orderBy('name')
.get()
// Override global validation settings for specific operations
const fastResults = await usersCollection
.limit(100)
.get({ validateOnRead: false })Native Firebase Access
For advanced Firestore features, you can access the native query object:
const query = usersCollection.where('name', '!=', '').orderBy('name')
const nativeQuery = query.native // Firebase Query object
const snapshot = await nativeQuery.get()Validation
Runtime Validation with Typia
FirestoreTyped integrates with typia for runtime validation:
import typia from 'typia'
interface UserEntity {
name: string
email: string
createdAt: Date
updatedAt: Date
}
const userEntityValidator = typia.createAssert<UserEntity>()
const db = getFirestoreTyped()
const usersCollection = db.collection<UserEntity>('users', userEntityValidator)
// This will throw a validation error if data doesn't match UserEntity
await usersCollection.doc('user-001').set(invalidData)Advanced Typia Features
Typia supports JSDoc comment-based type annotations for more precise validation:
interface UserEntity {
/**
* User ID with specific format
* @format uuid
*/
id: string
/**
* User name with length constraints
* @minLength 2
* @maxLength 50
*/
name: string
/**
* Email address with format validation
* @format email
*/
email: string
/**
* User age as integer
* @type integer
* @minimum 0
* @maximum 120
*/
age: number
/**
* Phone number with regex pattern
* @pattern ^\+?[1-9]\d{1,14}$
*/
phone: string
/**
* User status from specific values
* @items.enum ["active", "inactive", "pending"]
*/
status: "active" | "inactive" | "pending"
createdAt: Date
updatedAt: Date
}
const userValidator = typia.createAssert<UserEntity>()
const db = getFirestoreTyped()
// This will validate all the constraints:
// - id must be valid UUID format
// - name must be 2-50 characters
// - email must be valid email format
// - age must be integer between 0-120
// - phone must match the regex pattern
// - status must be one of the allowed values
await db.collection<UserEntity>('users', userValidator).doc('user123').set({
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'John Doe',
email: '[email protected]',
age: 30,
phone: '+1234567890',
status: 'active',
createdAt: new Date(),
updatedAt: new Date()
})Supported JSDoc Tags:
@type integer- Validates integer numbers@format email|uuid|date|uri- Format validation@pattern <regex>- Regular expression validation@minimum/@maximum- Numeric range validation@minLength/@maxLength- String length validation@items.enum- Enum value validation
This provides much more precise validation than basic TypeScript types alone.
Alternative Validation Libraries
Note: FirestoreTyped accepts any validator function with signature (data: unknown) => T. Besides typia, we have also verified compatibility with Zod:
// Zod example
import { z } from 'zod'
const UserSchema = z.object({
name: z.string(),
email: z.string(),
createdAt: z.date(),
updatedAt: z.date()
})
const zodValidator = (data: unknown): UserEntity => {
return UserSchema.parse(data) // Throws on validation failure
}
const db = getFirestoreTyped()
const users = db.collection<UserEntity>('users', zodValidator)
// Joi example (untested)
import Joi from 'joi'
const userJoiSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().required(),
createdAt: Joi.date().required(),
updatedAt: Joi.date().required()
})
const joiValidator = (data: unknown): UserEntity => {
const { error, value } = userJoiSchema.validate(data)
if (error) throw error
return value
}
const db2 = getFirestoreTyped()
const users2 = db2.collection<UserEntity>('users', joiValidator)⚠️ Important: We have verified compatibility with typia and Zod. Other validation libraries like Joi should work in theory but have not been verified. Please test thoroughly and report any issues.
Validation Control
// Enable validation for a specific operation
await usersCollection.doc('user-001').set(data, { validateOnWrite: true })
// Disable validation for a specific operation
await usersCollection.doc('user-001').set(data, { validateOnWrite: false })Validation Errors
import { FirestoreTypedValidationError } from '@info-lounge/firestore-typed'
try {
await usersCollection.doc('user-001').set(invalidData)
} catch (error) {
if (error instanceof FirestoreTypedValidationError) {
console.log('Validation failed:', error.message)
console.log('Original error:', error.originalError)
}
}Error Handling
// Setup: Create typed collection
const db = getFirestoreTyped()
const usersCollection = db.collection<UserEntity>('users', userValidator)Document Not Found
try {
const snapshot = await usersCollection.doc('99999').get()
if (!snapshot.metadata.exists) {
console.log('User does not exist')
}
} catch (error) {
console.error('Error retrieving user:', error)
}Validation Errors
try {
await usersCollection.doc('user-001').set(invalidData)
} catch (error) {
if (error instanceof FirestoreTypedValidationError) {
// Handle validation error
}
}Document Already Exists
import { DocumentAlreadyExistsError } from '@info-lounge/firestore-typed'
try {
// This will fail if document already exists
await usersCollection.doc('user-001').set(data, { failIfExists: true })
} catch (error) {
if (error instanceof DocumentAlreadyExistsError) {
console.log('Document already exists at:', error.documentPath)
// Handle by updating instead of creating
await usersCollection.doc('user-001').merge(data)
}
}Automatic Type Conversion
FirestoreTyped automatically handles the conversion of JavaScript types to Firestore special types during write operations:
Supported Type Conversions
| JavaScript Type | Firestore Type | Description |
|----------------|----------------|-------------|
| Date | Timestamp | DateTime data conversion (see precision note below) |
| SerializedGeoPoint | GeoPoint | Geographic location data conversion |
| SerializedDocumentReference<TCollection, TDocument> | DocumentReference | Type-safe document reference restoration |
| Buffer / Uint8Array | Bytes | Passed through unchanged in both directions |
Values that are already native Firestore types (Timestamp, GeoPoint, DocumentReference) pass through unchanged, both in document data and as query operands.
⚠️ Important Note on Date/Timestamp Precision: JavaScript
Dateobjects have millisecond precision, while FirestoreTimestampobjects support nanosecond precision. When converting fromDatetoTimestamp, the nanosecond portion will always be000000(zero). This means any nanosecond-level precision from the original Firestore data will be lost during the conversion process.
Example Usage
import { GeoPoint, Timestamp } from 'firebase-admin/firestore'
import type { StoreEntity } from '@common/types/store.types'
// JavaScript types are automatically converted to Firestore types
const storeData = {
id: 'store-001',
name: 'Tokyo Electronics Store',
address: 'Shibuya, Tokyo',
phone: '+81-3-1234-5678',
category: {
type: 'DocumentReference',
path: 'categories/electronics',
collectionId: 'categories',
documentId: 'electronics'
} as SerializedDocumentReference<'categories', CategoryEntity>,
location: {
type: 'GeoPoint',
latitude: 35.6762,
longitude: 139.6503
},
createdAt: new Date('2024-01-01'),
updatedAt: new Date()
}
// Setup: Create typed collection and automatically converts during write:
// - Date → Timestamp
// - SerializedGeoPoint → GeoPoint
// - SerializedDocumentReference → DocumentReference
const db = getFirestoreTyped()
const storesCollection = db.collection<StoreEntity>('stores', storeValidator)
await storesCollection.doc('store-001').set(storeData)Processing Order
- Validation: Data is validated using typia assertions
- Type Conversion: JavaScript types converted to Firestore types
- Write: Data written to Firestore with proper types
Type-Safe Document References
FirestoreTyped provides enhanced type safety for document references through the SerializedDocumentReference interface:
Generic Type Parameters
import { SerializedDocumentReference } from '@info-lounge/firestore-typed'
// TCollection: Collection name (literal type)
// TDocument: Referenced document type (phantom type)
type ProductRef = SerializedDocumentReference<'products', ProductEntity>
// Type-safe reference with compile-time validation
const parentRef: ProductRef = {
type: 'DocumentReference',
path: 'products/parent-001',
collectionId: 'categories', // Must match 'products'
documentId: 'parent-001'
}Type Safety Benefits
- Collection Name Validation:
collectionIdmust match the generic type - Type Safety: TypeScript enforces correct collection-document type pairs
- Runtime Checking: typia validates the structure and values
- IDE Support: Full IntelliSense and autocomplete
Usage Example
// Creating products with parent references
const childProduct: ProductEntity = {
id: 'child-001',
name: 'Child Product',
category: 'electronics',
userId: 'user-001',
parent: {
type: 'DocumentReference',
path: 'products/parent-001',
collectionId: 'categories', // ✅ Type-safe
documentId: 'parent-001'
} as SerializedDocumentReference<'products', ProductEntity>,
createdAt: new Date(),
updatedAt: new Date()
}Performance
Benchmarks
Note: These performance benchmarks are based on comprehensive testing using typia validators. Performance may vary with other validation libraries.
Based on comprehensive performance testing:
Core Operations Performance
- Create Operations: High-performance document creation (avg. 57ms)
- Read Operations: Optimized data retrieval (avg. 17ms)
- Merge Operations: Efficient partial updates (avg. 44ms)
- Batch Operations: Optimized bulk operations (avg. 72ms)
Type Conversion Performance
- SerializedDocumentReference conversion: <0.1ms overhead per reference
- Complex nested structure processing: Maintains ~98% of baseline performance
- Type guard validation: Efficient type checking with minimal performance impact
Validation Overhead
- Runtime validation adds approximately 1.2% overhead (0.58ms)
- SerializedDocumentReference validation adds <0.1ms per reference
- Validation can be disabled per operation for performance-critical paths
- Type guards ensure safe conversion without performance penalties
Memory Usage
- Minimal memory footprint (271KB/operation)
- Efficient handling of large datasets with document references
- No memory leaks in continuous operations
- SerializedDocumentReference conversion uses minimal additional memory
Advanced Usage
Options Management
// Initialize with default options
const db = getFirestoreTyped()
// Get current options
const currentOptions = db.getOptions()
// Create new instance with different options
const strictDb = db.withOptions({ validateOnRead: true })Type Safety
// Setup: Create typed collection
const db = getFirestoreTyped()
const users = db.collection<UserEntity>('users', userValidator)
// TypeScript will enforce correct types
await users.doc('user-001').set({
name: 'John Doe',
email: '[email protected]',
createdAt: new Date(),
updatedAt: new Date()
})
// This will cause a compile-time error
// await users.doc('user-001').set({
// id: 'user-001',
// name: 'John Doe',
// email: 123 // Type error: number is not assignable to string
// })Working with Existing Types
// Integration with your application types including document references
import { SerializedDocumentReference } from '@info-lounge/firestore-typed'
import typia from 'typia'
// Define your entity types
interface UserEntity {
name: string
email: string
createdAt: Date
updatedAt: Date
}
interface ProductEntity {
id: string
name: string
category: string
price: number
parentCategory?: SerializedDocumentReference<'categories', CategoryEntity>
createdAt: Date
updatedAt: Date
}
// Create validators
const userValidator = typia.createAssert<UserEntity>()
const productValidator = typia.createAssert<ProductEntity>()
// Create single FirestoreTyped instance that can handle multiple entity types
const db = getFirestoreTyped()
// Create type-safe collections with their respective validators
const users = db.collection<UserEntity>('users', userValidator)
const products = db.collection<ProductEntity>('products', productValidator)
// Working with products that have parent references
const productWithParent: ProductEntity = {
id: 'prod-001',
name: 'Test Product',
category: 'electronics',
userId: 'user-001',
parent: {
type: 'DocumentReference',
path: 'categories/electronics-main',
collectionId: 'categories',
documentId: 'electronics-main'
} as SerializedDocumentReference<'products', ProductEntity>,
createdAt: new Date(),
updatedAt: new Date()
}Best Practices
1. Always Use Validators
FirestoreTyped requires validators for type safety and data integrity:
// ✅ Good: Always provide a validator per collection
const userValidator = typia.createAssert<UserEntity>()
const db = getFirestoreTyped()
const users = db.collection<UserEntity>('users', userValidator)
// ❌ Bad: Never skip validation
// This is not possible with FirestoreTyped's design2. Type Your Data Properly
// ✅ Good: Use specific types
interface UserEntity {
name: string
email: string
createdAt: Date
updatedAt: Date
}
// ❌ Bad: Avoid generic types
// Don't use 'any' or overly broad types3. Handle Errors Appropriately
import { FirestoreTypedValidationError } from '@info-lounge/firestore-typed'
// Setup: Create typed collection
const db = getFirestoreTyped()
const userCollection = db.collection<UserEntity>('users', userValidator)
try {
await userCollection.doc('user-id').set(userData)
} catch (error) {
if (error instanceof FirestoreTypedValidationError) {
// Handle validation error
console.error('Data validation failed:', error.message)
} else {
// Handle other errors
throw error
}
}4. Optimize Validation Settings
// For read-heavy operations, consider disabling read validation
const fastDb = getFirestoreTyped(undefined, {
validateOnRead: false, // Skip validation on reads for performance
validateOnWrite: true // Always validate writes for data integrity
})
const userCollection = fastDb.collection<UserEntity>('users', userValidator)
// Override per operation when needed
const data = await userCollection.doc('user-id').get({ validateOnRead: true })5. Use Collection Group Queries Efficiently
// ✅ Good: Use collection group queries for cross-collection searches
const productsGroup = db.collectionGroup<ProductEntity>('products', productValidator)
const electronicsProducts = await productsGroup
.where('category', '==', 'electronics')
.orderBy('name')
.get()
// ✅ Good: Regular collection queries for single collection
const userProducts = await db
.collection<ProductEntity>('users/user-001/products', productValidator)
.get()6. Performance Considerations
// ✅ For high-frequency operations, consider validation overhead
const performanceDb = getFirestoreTyped(undefined, {
validateOnRead: false, // Skip validation for read-heavy logging
validateOnWrite: false // Skip validation for high-frequency writes
})
const logs = performanceDb.collection<LogEntry>('logs', logValidator)
// ✅ Use batch operations for multiple writes (planned feature)
// const batch = db.batch() // ⚠️ Not yet implemented
// batch.set(userCollection.doc('user1'), userData1)
// batch.set(userCollection.doc('user2'), userData2)
// await batch.commit()
// For now, use native Firestore batch:
const batch = db.native.batch()
batch.set(db.native.collection('users').doc('user1'), userData1)
batch.set(db.native.collection('users').doc('user2'), userData2)
await batch.commit()API Reference
Factory Function
/**
* Creates a FirestoreTyped instance for multi-entity type support
* @param options - Global configuration options
* @returns Configured FirestoreTyped instance
* @example
* ```typescript
* import { getFirestoreTyped } from '@info-lounge/firestore-typed'
*
* const db = getFirestoreTyped(firestore, options)
* ```
*/
function getFirestoreTyped(
firestore?: Firestore,
options?: FirestoreTypedOptions
): FirestoreTypedFirestoreTyped Instance
/**
* Main FirestoreTyped instance providing type-safe Firestore operations
*/
class FirestoreTyped {
/**
* Gets a typed collection reference with validator
* @param path - Firestore collection path
* @param validator - Runtime validation function for the entity type
* @returns Type-safe collection reference
* @example
* ```typescript
* const usersCollection = db.collection<UserEntity>('users', userValidator)
* const userProductsCollection = db.collection<ProductEntity>('users/user-001/products', productValidator)
* ```
*/
collection<T>(path: string, validator: (data: unknown) => T): CollectionReference<T>
/**
* Gets a typed collection group reference with validator
* @param collectionId - Collection group ID to query across multiple parent documents
* @param validator - Runtime validation function for the entity type
* @returns Type-safe collection group reference
* @example
* ```typescript
* const allPosts = db.collectionGroup<PostEntity>('posts', postValidator)
* const userPosts = allPosts.where('userId', '==', 'user123')
* ```
*/
collectionGroup<T>(collectionId: string, validator: (data: unknown) => T): CollectionGroup<T>
/**
* Gets current configuration options
* @returns Current FirestoreTyped options
* @example
* ```typescript
* const currentOptions = db.getOptions()
* console.log(`Validation on read: ${currentOptions.validateOnRead}`)
* ```
*/
getOptions(): ResolvedFirestoreTypedOptions
/**
* Creates new instance with modified options
* @param options - Partial options to override
* @returns New FirestoreTyped instance with updated options
* @example
* ```typescript
* const strictDb = db.withOptions({ validateOnRead: true })
* const fastDb = db.withOptions({ validateOnWrite: false })
* ```
*/
withOptions(options: Partial<FirestoreTypedOptions>): FirestoreTyped
/**
* Access to native Firestore instance for advanced operations
* @returns Native Firestore instance
* @example
* ```typescript
* const nativeFirestore = db.native
* const batch = nativeFirestore.batch()
* ```
*/
get native(): Firestore
}CollectionReference Class
/**
* Type-safe collection reference with query capabilities
*/
class CollectionReference<T> {
/**
* Collection ID (last segment of path)
* @example 'users', 'products'
*/
get id(): string
/**
* Full collection path
* @example 'users', 'users/user-001/products'
*/
get path(): string
/**
* Gets a document reference within this collection
* @param id - Document ID
* @returns Type-safe document reference
* @example
* ```typescript
* const userDoc = usersCollection.doc('user123')
* const userData = await userDoc.get()
* ```
*/
doc(id: string): DocumentReference<T>
/**
* Adds a new document with auto-generated ID
* @param data - Document data to add
* @param options - Write options (validation settings)
* @returns Promise resolving to document reference
* @throws FirestoreTypedValidationError if validation fails
* @example
* ```typescript
* const docRef = await usersCollection.add({
* name: 'John Doe',
* email: '[email protected]',
* createdAt: new Date(),
* updatedAt: new Date()
* })
* console.log(`Created with ID: ${docRef.id}`)
* ```
*/
add(data: T, options?: WriteOptions): Promise<DocumentReference<T>>
/**
* Gets all documents in the collection
* @param options - Read options (validation settings)
* @returns Promise resolving to query snapshot
* @throws FirestoreTypedValidationError if validation fails
* @example
* ```typescript
* const snapshot = await usersCollection.get()
* const users = snapshot.docs.map(doc => doc.data!)
* console.log(`Found ${users.length} users`)
* ```
*/
get(options?: ReadOptions): Promise<QuerySnapshot<T>>
/**
* Creates a query with where clause
* @param field - Field to filter on
* @param operator - Comparison operator
* @param value - Value to compare against
* @returns Query builder for method chaining
* @example
* ```typescript
* const activeUsers = await usersCollection
* .where('status', '==', 'active')
* .get()
* ```
*/
where(field: keyof T, operator: WhereFilterOp, value: any): Query<T>
/**
* Orders query results by specified field
* @param field - Field to order by
* @param direction - Sort direction ('asc' | 'desc')
* @returns Query builder for method chaining
* @example
* ```typescript
* const sortedUsers = await usersCollection
* .orderBy('createdAt', 'desc')
* .limit(10)
* .get()
* ```
*/
orderBy(field: keyof T, direction?: 'asc' | 'desc'): Query<T>
/**
* Limits number of results
* @param limit - Maximum number of documents to return
* @returns Query builder for method chaining
*/
limit(limit: number): Query<T>
}DocumentReference Class
/**
* Type-safe document reference for individual document operations
*/
class DocumentReference<T> {
/**
* Document ID
*/
get id(): string
/**
* Full document path
* @example 'users/user123', 'users/user-001/products/prod456'
*/
get path(): string
/**
* Gets document data
* @param options - Read options (validation settings)
* @returns Promise resolving to document snapshot
* @throws FirestoreTypedValidationError if validation fails
* @example
* ```typescript
* const snapshot = await userDoc.get()
* if (snapshot.metadata.exists) {
* console.log(`User: ${snapshot.data!.name}`)
* } else {
* console.log('User not found')
* }
* ```
*/
get(options?: ReadOptions): Promise<DocumentSnapshot<T>>
/**
* Sets document data (creates or replaces)
* @param data - Complete document data
* @param options - Write options (validation settings, failIfExists)
* @returns Promise resolving when operation completes
* @throws FirestoreTypedValidationError if validation fails
* @throws DocumentAlreadyExistsError if failIfExists is true and document exists
* @example
* ```typescript
* // Normal set operation
* await userDoc.set({
* id: 'user123',
* name: 'John Doe',
* email: '[email protected]',
* createdAt: new Date(),
* updatedAt: new Date()
* })
*
* // Ensure document doesn't exist
* await userDoc.set(data, { failIfExists: true })
* ```
*/
set(data: T, options?: WriteOptions): Promise<void>
/**
* Merges partial data with existing document
* @param data - Partial document data to merge
* @param options - Write options (validation settings)
* @returns Promise resolving when operation completes
* @throws DocumentNotFoundError if document doesn't exist
* @throws FirestoreTypedValidationError if validation fails
* @example
* ```typescript
* await userDoc.merge({
* email: '[email protected]',
* updatedAt: new Date()
* })
*
* // Error handling
* try {
* await userDoc.merge(data)
* } catch (error) {
* if (error instanceof DocumentNotFoundError) {
* // Document doesn't exist, create it instead
* await userDoc.set(fullData)
* }
* }
* ```
*/
merge(data: Partial<T>, options?: WriteOptions): Promise<void>
/**
* Deletes the document
* @returns Promise resolving when deletion completes
* @example
* ```typescript
* await userDoc.delete()
* console.log('User deleted successfully')
* ```
*/
delete(): Promise<void>
}Types
interface FirestoreTypedOptions {
validateOnRead?: boolean
validateOnWrite?: boolean
}
interface CollectionOptions<T> {
validator?: (data: unknown) => T
}
interface ReadOptions {
validateOnRead?: boolean
}
interface WriteOptions {
validateOnWrite?: boolean
failIfExists?: boolean // If true, throws error if document already exists
}
interface DocumentSnapshot<T> {
metadata: DocumentMetadata
data?: T
}
interface QuerySnapshot<T> {
docs: DocumentSnapshot<T>[]
empty: boolean
size: number
}