Typed Routes
Generated types and safe param passing between screens. Typed routes turn your app/ filesystem into a TypeScript Href union - invalid paths fail tsc before they ship.
Search across all documentation pages
Generated types and safe param passing between screens. Typed routes turn your app/ filesystem into a TypeScript Href union - invalid paths fail tsc before they ship.
Quick-reference recipe card - copy-paste ready.
// app.config.ts
export default {
experiments: {
typedRoutes: true,
},
} satisfies import("expo/config").ExpoConfig;# Regenerate types after adding routes
npx expo start
# or
npm run typecheck # tsc --noEmitimport { Link, router, type Href } from "expo-router";
// ✅ Static route - string href
const settings: Href = "/settings";
// ✅ Dynamic route - object with params
const order: Href = { pathname: "/orders/[id]", params: { id: "42" } };
<Link href={order}>Open order</Link>
router.push(order);// src/navigation/hrefs.ts
import type { Href } from "expo-router";
export const orderDetailHref = (id: string): Href => ({
pathname: "/orders/[id]",
params: { id },
});When to reach for this:
router.push in hooks - typos are invisible at runtimetsc --noEmitEnable typed routes, define href builders, consume in screens, and validate runtime params.
// app.config.ts
import type { ExpoConfig, ConfigContext } from "expo/config";
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: "TypedRoutesApp",
slug: "typed-routes-app",
experiments: {
typedRoutes: true,
},
});app/
├── _layout.tsx
├── settings.tsx
└── orders/
├── index.tsx
└── [id].tsx// src/navigation/hrefs.ts
import type { Href } from "expo-router";
export const hrefs = {
settings: "/settings" as Href,
orders: "/orders" as Href,
orderDetail: (id: string): Href => ({
pathname: "/orders/[id]",
params: { id },
}),
} as const;// features/orders/ui/OrdersListScreen.tsx
import { Link } from "expo-router";
import { Text, View } from "react-native";
import { hrefs } from "@/navigation/hrefs";
export function OrdersListScreen() {
return (
<View style={{ padding: 16 }}>
<Link href={hrefs.orderDetail("99")}>
<Text>Order 99</Text>
</Link>
</View>
);
}// features/orders/ui/OrderDetailScreen.tsx
import { useLocalSearchParams } from "expo-router";
import { z } from "zod";
import { Text, View } from "react-native";
const paramsSchema = z.object({
id: z.string().min(1),
});
export function OrderDetailScreen() {
const raw = useLocalSearchParams();
const parsed = paramsSchema.safeParse(raw);
if (!parsed.success) {
return (
<View style={{ padding: 16 }}>
<Text>Invalid order link</Text>
</View>
);
}
const { id } = parsed.data;
return (
<View style={{ padding: 16 }}>
<Text>Order {id}</Text>
</View>
);
}// Compile-time failure examples (do not ship):
// router.push("/orders"); // ❌ missing params for [id] route
// router.push("/settigns"); // ❌ typo in static path
// <Link href="/unknown" /> // ❌ route does not exist in app/.expo/types/router.d.ts # Auto-generated - do not hand-editapp/.expo/; CI runs expo start or npx expo customize tsconfig flow so types exist before tsctsconfig.json should include Expo's types:{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts"]
}| Type | Purpose |
|---|---|
Href | Union of all valid href values for Link and router |
Href<T> | Narrow href for a specific route |
| Route path strings | Literal union of static segments |
import type { Href } from "expo-router";
function navigate(href: Href) {
router.push(href);
}app/users/[userId]/posts/[postId].tsxconst href: Href = {
pathname: "/users/[userId]/posts/[postId]",
params: { userId: "1", postId: "9" },
};tscrouter.d.ts when unsure[...slug].tsx generate array or string param types depending on configurationCompile-time (typed routes): Developer typos in router.push / Link
Runtime (Zod): Deep links, push notifications, malformed URLsTyped routes do not validate external input - always parse useLocalSearchParams() before fetching.
npx expo customize tsconfig.json # first-time setup if needed
npx expo start --non-interactive & # generate .expo/types
sleep 5
npm run typecheckAdd to PR checks when app/ changes - route renames without updated hrefs break builds loudly (desired).
Types stale after rename - tsc passes locally with old .expo/types until Metro restarts. Fix: Restart dev server or run typecheck in CI fresh each time.
Assuming types validate notification payloads - push opens /orders/abc with extra query junk. Fix: Zod at screen boundary.
String interpolation bypasses checks - router.push(\/orders/${id}` as Href)defeats the system. **Fix:** Object href builders withoutas Href` casts.
Monorepo app path not scanned - types generated for wrong workspace root. Fix: Run Expo CLI from the app package directory.
Missing experiments.typedRoutes - Href widens to string. Fix: Enable in app.config.ts on day one.
Shared package imports app types - coupling the wrong direction. Fix: packages/navigation exports builders; app owns generated types.
Route groups in pathname - typed hrefs use URL paths, not (tabs) group names. Fix: /settings not /(tabs)/settings in href objects.
| Alternative | Use When | Don't Use When |
|---|---|---|
experiments.typedRoutes | Expo Router apps on SDK 57 | Non-Expo React Navigation codebases |
Manual routes.ts const object | Tiny apps (<8 routes) | Fast-growing app/ trees |
| Zod-only validation | Untyped brownfield | Greenfield - add typed routes too |
| Codegen from OpenAPI paths | Backend-driven web parity | Mobile-first file-based routing |
ESLint no-restricted-syntax on string push | Enforcing migration | You have typed routes enabled already |
// app.config.ts
experiments: { typedRoutes: true }Restart Metro. Confirm .expo/types/router.d.ts exists. Run tsc --noEmit.
.expo/types/router.d.ts - auto-generated from files in app/. Do not edit manually; restart the dev server after route changes.
Generated types updated - existing string hrefs may now be invalid. Update callsites to use the new path or add missing params for dynamic segments.
Yes - groups like (tabs) are omitted from URL types. /settings is valid; /(tabs)/settings is typically not in the Href union.
const { id } = useLocalSearchParams<{ id: string }>();Generated types may also export route-specific param maps - inspect router.d.ts. Still validate with Zod for external entry points.
Yes - run type generation per app package (apps/mobile). Share href builder functions from packages/navigation; each app validates against its own app/ tree.
app/docs/[...slug].tsx generates href types requiring slug - often string | string[]. Test deep links with multiple segments in Maestro smoke flows.
Avoid as Href casts - they silence real errors. Fix the path or use object form with required params.
No - types are compile-time only. Zero production JS cost.
Features import orderDetailHref from @/navigation/hrefs - never import from .expo/types directly. The builder return type is Href.
Link, router, and prefetch[id] segmentsRedirect hreftsc --noEmit in CIStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026