Skip to content

fix(mobile): generate the type declarations the tsconfig already includes - #108

Merged
toruiwasa merged 2 commits into
mainfrom
fix/mobile-generated-types
Sep 2, 2026
Merged

fix(mobile): generate the type declarations the tsconfig already includes#108
toruiwasa merged 2 commits into
mainfrom
fix/mobile-generated-types

Conversation

@toruiwasa

@toruiwasa toruiwasa commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Closes #105

apps/mobile/app.json sets experiments.typedRoutes: true and tsconfig.json includes .expo/types/**/*.ts and expo-env.d.ts — but nothing ever wrote those files, so both type gates were inert. #11 added the typecheck task that made this visible; this PR gives that compiler the declarations the configuration already claims it has.

Decision record: plans/ISSUE-105_Mobile_Generated_Types.md

What changed

"typegen":   "EXPO_NO_TYPESCRIPT_SETUP=1 expo customize tsconfig.json",
"typecheck": "pnpm run typegen && tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.spec.json"

turbo.json is unchanged; .github/workflows/ci.yml changes in a comment only. Generation is the first step of the package's own typecheck script, so #11's invariant — pnpm typecheck is the gate, passes locally ⇒ passes in CI — still holds. A CI-only step would have checked something the developer's machine never checks.

expo customize tsconfig.json is what @expo/cli's type-generation/routes.js documents as the way to run typed routes without Metro or a dev server; measured at ~2.2s with no bundler process. EXPO_NO_TYPESCRIPT_SETUP=1 suppresses the prerequisite that would otherwise run expo install for typescript / @types/react — i.e. network access and a package.json write — from inside a CI check; generation does not read the variable, so the gate is unaffected.

The files are generated, not committed: router.d.ts is derived from the app/ route tree, so a committed copy goes stale as a false pass — the worst failure shape for a gate whose whole point is not being vacuous.

Live defect found and fixed

Generating the route types surfaced this from the #94 scaffold:

href: ... | `/_sitemap...` | `/__tests__/index.test...`

Expo Router turns every file under app/ into a route — _ctx.*.js's require.context regex excludes only +api / +html / +middleware, and getRoutesCore.js's ignore list adds nothing for tests. So app/__tests__/index.test.tsx was a real /__tests__/index.test route and shipped @testing-library/react-native inside the app bundle.

Tests move to apps/mobile/__tests__/. No tsconfig change was needed (**/__tests__/** matches at the project root too); collectCoverageFrom also stops counting the test file as a source file, which it had been doing.

Two of #105's premises were wrong

  1. expo export does not generate these files. startTypeScriptServices has exactly two callers in @expo/cli 57.0.17 — DevServerManager (expo start) and customize/typescript.js. So typedRoutes and expo-env.d.ts are declared but never generated — both type gates are inert #105 is independent of Nothing automated exercises Metro's package-exports resolution #96, not sequenced behind it. plans/REQ-17_Task8_CI_Mobile.md carries a dated correction where that hypothesis was written.

  2. A misspelled EXPO_PUBLIC_* cannot be rejected on a bare reference. expo/types/metro-require.d.ts declares ProcessEnv with an open index signature ([key: string]: string | undefined), so any name reads. Measured:

    type of process.env.EXPO_PUBLIC_TYPO const s: string = <that>
    without expo-env.d.ts any exit 0 — silent
    with expo-env.d.ts string | undefined exit 2, TS2322

    The achievable gate is "a misspelled variable is not a string". Declaring the real names lands with the code that reads them (Task 9 — Mobile core infrastructure (logger, supabase client, query client, auth store, constants, colors) #12).

Verified non-vacuous

Every probe was run after rm -rf .expo/types expo-env.d.ts, so it also proves generation happens inside the same typecheck invocation rather than relying on leftovers.

Probe Result
<Redirect href="/this-route-does-not-exist" /> pnpm typecheckexit 2, TS2322
const url: string = process.env.EXPO_PUBLIC_TYPO pnpm typecheckexit 2, TS2322
route added in the same run appears in the accepted href union — generation is not stale
lint with no generated files exit 0, generates nothing — CI's lint-before-typecheck order is safe
pnpm lint / pnpm typecheck / pnpm test 0 / 0 / 0
git status afterwards clean — Expo rewrites tsconfig.json / .gitignore only when extends or the two include entries are missing, and ours has all three

Probes for both gates are now permanent in apps/mobile/README.md, in the exact form they were run.

Second commit — found in adversarial review

Reviewing the first commit for holes found a regression it introduced: moving the tests out of app/ moved them out of expo lint's reach. The wrapper lints only src / app / components (DEFAULT_INPUTS in @expo/cli's lint/lintAsync.js). Verified by injection — a no-var error in __tests__/index.test.tsx passed expo lint at exit 0 while the same error in app/index.tsx failed it.

Fix: "lint": "eslint ." — lints everything the flat config does not ignore, so there is no directory list to keep in sync. Explicit inputs to expo lint were rejected: unlike the defaults they skip the existence filter, so the list breaks on a missing directory and goes stale on a new one. Measured: clean pass on the current tree, exit 1 on the injected error, exit 0 with the generated files absent.

…udes

apps/mobile/app.json sets experiments.typedRoutes and tsconfig.json includes
.expo/types/**/*.ts and expo-env.d.ts, but nothing ever wrote those files. Both
type gates were inert: <Redirect href="/does-not-exist" /> type-checked at exit
0, and process.env was `any` rather than `string | undefined`. #11 added the
typecheck gate that made this visible.

Generation now runs as the first step of the package's typecheck script, via
`expo customize tsconfig.json` — which @expo/cli's type-generation/routes.js
documents as the way to run typed routes without Metro or a dev server, and
which measures at ~2.2s with no bundler. EXPO_NO_TYPESCRIPT_SETUP=1 suppresses
the prerequisite that would otherwise `expo install` typescript/@types/react
from inside a CI check; it does not affect generation. turbo.json and the CI
workflow are unchanged, so `pnpm typecheck` stays the whole gate.

The files are generated rather than committed: router.d.ts is derived from the
app/ route tree and a committed copy would go stale as a false pass.

Generating them also exposed a live defect from the #94 scaffold. Expo Router
turns every file under app/ into a route — its require.context regex excludes
only +api/+html/+middleware and getRoutesCore's ignore list adds nothing for
tests — so app/__tests__/index.test.tsx was a real /__tests__/index.test route
and shipped @testing-library/react-native in the app bundle. Tests move to
apps/mobile/__tests__/. No tsconfig change was needed; both patterns match at
the project root.

Two of #105's premises were wrong and are corrected in the plan file: `expo
export` does not generate these files (so this is independent of #96, not
sequenced behind it), and a misspelled EXPO_PUBLIC_* cannot be rejected on a
bare reference — expo/types declares ProcessEnv with an open index signature,
so the achievable gate is that the value is not a `string`.

Closes #105

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
pulseticker Ready Ready Preview Sep 2, 2026 1:39pm UTC

…d tests

Adversarial review of the previous commit found a regression it introduced:
`expo lint` lints only src/, app/ and components/ (DEFAULT_INPUTS in
@expo/cli's lint/lintAsync.js), so moving the tests out of app/ moved them out
of lint's reach. Verified by injection: a no-var error in
__tests__/index.test.tsx passed `expo lint` at exit 0 while the same error in
app/index.tsx failed it.

The lint script now runs `eslint .`, which lints everything the flat config
does not ignore — no directory list to keep in sync. Passing explicit inputs
to expo lint instead was rejected: unlike the defaults they skip the existence
filter, so the list breaks on a missing directory and goes stale on a new one.

Measured: eslint . passes clean on the current tree, fails on the injected
error, and passes with the generated type files absent, so CI's
lint-before-typecheck order is unaffected. ci.yml changes in a comment only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@toruiwasa
toruiwasa merged commit 4f86f7e into main Sep 2, 2026
4 checks passed
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.

typedRoutes and expo-env.d.ts are declared but never generated — both type gates are inert

1 participant