Skip to content

fix: tighten ReactFireOptions generic types, remove T | any widening#740

Open
tyler-reitz wants to merge 13 commits into
FirebaseExtended:mainfrom
tyler-reitz:fix/reactfire-options-generic-types
Open

fix: tighten ReactFireOptions generic types, remove T | any widening#740
tyler-reitz wants to merge 13 commits into
FirebaseExtended:mainfrom
tyler-reitz:fix/reactfire-options-generic-types

Conversation

@tyler-reitz

@tyler-reitz tyler-reitz commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #383.

  • Removes | any from initialData and startWithValue in ReactFireOptions<T>, so passing a value of the wrong type is now a TypeScript error rather than silently accepted
  • Tightens checkIdField to read options.idField directly (already typed string | undefined) instead of routing through checkOptions
  • Removes checkinitialData and checkOptions (both exported but had zero call sites in the codebase)
  • Adds a type cast in useObservable at the initialData assignment to handle the internal mismatch between a hook's user-facing T and the raw Firebase observable type (e.g. DocumentSnapshot<T> vs T)
  • Adds casts at rxfire docData/collectionData call sites where rxfire expects keyof T but the field is a plain string — a pre-existing gap in rxfire's types that T | any was silently masking

Breaking changes

Three consumer-visible breaks:

  1. initialData / startWithValue type enforcement. Any call site passing an initialData value whose type does not match the hook's T will now get a compile error. Previously T | any collapsed to any and accepted anything silently. For the raw snapshot hooks (useFirestoreDoc, useFirestoreCollection), a DocumentSnapshot<T> previously compiled as initialData and now errors — see Known Limitation below.

  2. checkinitialData removal. This function was exported but had zero call sites in the codebase. Any code importing it from 'reactfire' will get a compile error at build time and a module load error at runtime for plain JS importers (ESM import error at load).

  3. checkOptions removal. Same situation — exported, zero internal call sites. Compile error for TS consumers, runtime module load error for plain JS importers.

Known limitation

initialData in ReactFireOptions<T> is typed as T, which works correctly for the data hooks (useFirestoreDocData, useFirestoreCollectionData, useDatabaseObjectData, etc.). For the raw snapshot hooks (useFirestoreDoc, useFirestoreDocOnce, useFirestoreCollection), the correct initialData type would be DocumentSnapshot<T> / QuerySnapshot<T>, not T. This mismatch is pre-existing and not introduced here. Tracked in #741 for follow-up.

Test plan

  • npm test passes (functions emulator test failures are pre-existing and unrelated)
  • npx tsc --noEmit exits clean

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@armando-navarro armando-navarro left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tightening looks right to me, and everything I checked came back clean. Two asks below, one decision that belongs to Jeff, and some optional notes.

What I verified

  • Packed this branch (merged with main) and type-checked a consumer fixture against it with skipLibCheck: false, on both React 18 and React 19 type packages: wrong-typed initialData and startWithValue are rejected, checkIdField is string | undefined, and every correct usage still compiles.
  • The new casts match rxfire 6.1.0's declarations: docData wants keyof R, collectionData wants (U | keyof T) & keyof NonNullable<T>, and database keyField is a plain string, which is why only the firestore call sites need casts.
  • Runtime unchanged where it counts: firestore (11/11) and useObservable (14/14) suites pass on the merged result.

Ask 1: commit regenerated reference docs

npm run docs on this branch changes 8 files under docs/reference/, including deleting functions/checkinitialData.md, so the docs workflow will fail. It has not had a chance to yet: a conflicted PR triggers no workflows at all, so the checks list here is empty rather than green, and nothing has run tests or type checks on this PR either.

Ask 2: three description corrections (the squash body becomes the changelog)

  • The checkinitialData removal is a runtime break for plain JS importers (ESM import error at load), not TypeScript-only.
  • checkOptions' declared return type moves from any to unknown (visible in the emitted dist/index.d.ts); worth listing as a third consumer-visible change.
  • For the four snapshot hooks, the T-vs-snapshot mismatch is pre-existing but the enforcement is new: a runtime-correct initialData (a DocumentSnapshot, matching what the hook returns) compiled on main and is now a compile error, while the type demands a plain T that would break a consumer calling .data() on it. Verified against the packed artifact.

Decision for Jeff: semver, and whether #741 rides along

Three consumer-visible breaks under a fix: title, and merges auto-publish. Worth shipping in my view, but how it ships is Jeff's call, same as #735 and #738. Related: should #741 land with this PR so the snapshot hooks never ship demanding the wrong initialData type?

Optional notes

  • When you rebase: #731 already restructured the block your useObservable.ts change touches, and your branch type-checks clean on current main with that hunk dropped entirely. Only index.ts and firestore.tsx need to survive.
  • checkOptions now has zero call sites in src/, the same rationale used to remove checkinitialData; either say why it stays or remove both in the same breaking release.
  • A small type-assertions file with @ts-expect-error on the lines this PR fixes would ride the existing tsc --noEmit CI (both React jobs) as a regression test. Happy to share the fixture I used.
  • #738 touches the same idField lines in firestore.tsx; whichever merges second needs another rebase pass.

If I have any of this wrong, say so and I will dig back in. Once the docs and description are in, this is a quick approve from me.

@armando-navarro armando-navarro left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for turning these around, the description reads accurately now and dropping checkOptions is clean. Approving. Two follow-ups to fold into the rebase pass this needs anyway, neither of which changes the approval, and one of them is my own suggestion coming back to bite.

The type regression test doesn't catch a re-widening

This one is on me for suggesting it without pinning down the mechanics. As written, src/reactfire-options.type-test.ts passes whether or not the tightening is in place, so it would not fail if initialData went back to T | any.

The cause is noUnusedLocals: the negative-case consts are never read, so each line always carries a TS6133 ("declared but its value is never read"). @ts-expect-error is satisfied by any error on the next line, so that unused-local error keeps the directive "used" even when the type error it is meant to catch disappears. I confirmed it locally: reverting initialData?: T to initialData?: T | any leaves tsc --noEmit green.

Consuming each const fixes it, so the type error is the only diagnostic on the line:

// @ts-expect-error initialData must be T, not a different type
const _wrongInitialData: ReactFireOptions<string> = { initialData: 123 };
void _wrongInitialData;

With that, reverting the fix makes the line error-free, the directive goes unused, and tsc fails as intended. One small aside: the file rides in the published tarball via files: ["dist","src"] and is the only test-only file under src/, so a type-tests/ dir or an npmignore would keep it out of consumers' installs.

One regenerated doc file will fail the docs check

docs/reference/classes/ReactFireError.md as committed documents the inherited Error.stackTraceLimit and captureStackTrace statics, but a fresh npm run docs omits them. I checked this against main to be sure it wasn't my setup: a clean npm ci + npm run docs on current main regenerates its committed docs identically here, and both main and this branch pin the same @types/node (25.9.3), so that regen is the canonical one. It looks like this file was generated against an older @types/node. Since docs.yaml regenerates and diffs, it would fail on that file until it is regenerated in a clean environment.

You will hit this naturally on the rebase: the branch is still conflicted, so no substantive CI (Build, tests, the React 18/19 type checks, docs) has run yet, and rebasing onto current main means regenerating the docs again anyway. When you rebase, the useObservable.ts hunk also drops out (superseded by #731).

The semver call stays with Jeff, same as before. Everything else checks out, which is why this is an approve rather than a blocker.

dependabot Bot and others added 13 commits July 21, 2026 10:50
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.2.0 to 4.5.0.
- [Changelog](https://github.com/adobe/css-tools/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-version: 4.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.22.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.2/History.md)
- [Commits](expressjs/express@4.18.2...v4.22.2)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
FirebaseExtended#729)

* fix: use globalThis.performance in SuspenseWithPerf to avoid SSR crash

* fix: use useEffect instead of useLayoutEffect in SuspenseWithPerf

useLayoutEffect triggers a React warning on the server. Since performance
marks don't require synchronous DOM layout reads, useEffect is sufficient
and avoids the SSR warning.
…ebaseExtended#733)

* fix: update useFirestoreDocData return type to include undefined

docData() from rxfire returns Observable<T | undefined> because a
Firestore document may not exist. The previous ObservableStatus<T>
return type was a lie — callers would get undefined at runtime with
no type-level warning.

Fixes FirebaseExtended#577

* docs: regenerate API reference after ObservableStatus type fix
…irebaseExtended#732)

* fix: replace JSX.Element with React.ReactElement for React 19 compat

JSX.Element was removed from the global JSX namespace in React 19 types.
Components returning JSX.Element are no longer assignable to the updated
ReactNode return type, causing type errors for users on React 19.

Fixes FirebaseExtended#637

* Update src/storage.tsx

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update src/storage.tsx

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* docs: regenerate API reference after React 19 type fixes

* ci: trigger checks

* docs: regenerate API reference without skipErrorChecking flag

* ci: add React 18/19 type-check matrix job

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Removes the `| any` escape hatch from `initialData` and `startWithValue`
in ReactFireOptions<T>, so passing a mismatched type is now a compile
error instead of silently accepted.

Also tightens the return types of `checkIdField` and `checkinitialData`,
and adds a cast in `useObservable` to handle the internal mismatch
between the hook's T and the raw Firebase observable type.

Closes FirebaseExtended#383
checkIdField now reads options.idField directly (already string | undefined)
instead of routing through checkOptions and casting. checkinitialData had
no call sites and is removed.
@tyler-reitz
tyler-reitz force-pushed the fix/reactfire-options-generic-types branch from b1dba42 to a10f992 Compare July 21, 2026 17:51
@tyler-reitz

Copy link
Copy Markdown
Contributor Author

Thanks Armando, both follow-ups addressed in the rebase:

Type regression test: Added void _wrongInitialData; and void _wrongStartWithValue; after each negative-case const. Verified locally: reverting initialData?: T back to T | any now causes tsc to fail with two unused @ts-expect-error directives as intended.

npmignore: Added .npmignore with src/*.type-test.ts to keep the file out of the published tarball.

Docs: Regen after rebase produced no changes, so the ReactFireError.md statics issue resolved itself on the rebase.

Branch rebased and force-pushed.

Comment thread src/firestore.tsx
/**
* Get a Firestore document, unwrap the document into a plain object, and don't subscribe to changes
*/
export function useFirestoreDocDataOnce<T = unknown>(ref: DocumentReference<T>, options?: ReactFireOptions<T>): ObservableStatus<T | undefined> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have missed this elsewhere, but what's the rationale for widening to T | undefined? It looks like this gets cast to ObservableStatus<T> in the return statement

Comment thread src/index.ts
suspense?: boolean;
}

export function checkOptions(options: ReactFireOptions, field: string) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any of these are actually used, and agree deleting is the right thing, but this is technically a breaking change. Let's leave this for now, since it isn't hurting anything (unless it is, in which case let me know)

Please add an issue reminding us to remove all of these in v5

Comment thread src/performance.tsx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would useInitPerformance be a good option here?

export const useInitPerformance: InitSdkHook<FirebasePerformance> = (initializer, options) =>

Comment thread src/performance.tsx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change to React.ReactElement happens in a bunch of spots, and could potentially be a breaking change. What are the benefits and risks of switching?

Comment thread src/storage.tsx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ReactNode vs ReactElement?

Comment thread src/useObservable.ts
status: 'success',
hasEmitted: true,
} as ObservableStatus<T>;
update.data = (config?.initialData ?? config?.startWithValue) as T | undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did TypeScript have trouble inferring this?

Comment thread README.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ReactFire is designed for **web React apps** and wraps the [Firebase JavaScript SDK](https://firebase.google.com/docs/web/setup). It is not compatible with React Native or Expo. For React Native projects, use [react-native-firebase](https://rnfirebase.io/) instead.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: enforce generic types when passed to ReactFireOptions

3 participants