HomeGuidesReferenceLearn
ArchiveExpo SnackDiscord and ForumsNewsletter

Typed routes

Learn how to use statically typed links and routes in Expo Router.


Available from Expo SDK 49 and Expo Router v2 when using TypeScript. Expo Router supports standard TypeScript out of the box. See the TypeScript guide for more information on how to set it up.

Expo Router supports generating TypeScript types automatically with Expo CLI. This enables <Link>, and the hooks API to be statically typed. This feature is currently in beta and is not enabled by default.

Get started

Quick start

If you created your project using the Expo Router quick start guide then your project is already configured to use typed routes. The Expo CLI will generate the required type file the first time you run npx expo start. Then, you can use autocomplete for href props whenever you use an Expo Router <Link> component in a .tsx file.

Manual configuration

While the feature is in beta, you can enable it by setting experiments.typedRoutes to true in your app.json:

app.json
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

Run npx expo customize tsconfig.json to configure your tsconfig.json to add the required includes fields.

Then, start the development server by running npx expo start. You can now use autocomplete in the Expo Router <Link> component's href prop.

Type generation

Typed routes in Expo Router are automatically generated when the development server starts. By default, these generated types are configured to be untracked by Git and will be added to the local .gitignore file. This ensures that autogenerated files do not clutter your version control system.

If you find yourself in a situation where you need to generate these types without initiating the development server, such as during type checking on a Continuous Integration (CI) server. To do this, run the command npx expo customize tsconfig.json on the CI.

Statically typed routes

Components and functions that use Href<T> will now be statically typed and have a much stricter definition. For example:

<Link href="/about" /><Link href="/user/1" /><Link href={`/user/${id}`} /><Link href={("/user" + id) as Href} />
// TypeScript errors if href is not a valid route<Link href="/usser/1" />

For dynamic routes, Href's need to be objects and their parameters are strictly typed:

<Link href={{ pathname: "/user/[id]", params: { id: 1 }}} />
// TypeScript errors as href is valid, but it should be a HrefObject with params<Link href="/user/[id]" />
// TypeScript errors as params contain invalid keys<Link href={{ pathname: "/user/[id]", params: { _id: 1 }}} />
// TypeScript errors as params contain unknown keys<Link href={{ pathname: "/user/[id]", params: { id: 1, id2: 2 }}} />

Relative paths

Statically typed routes do not support relative paths. You'll need to use absolute paths for all routes:

<Link href="/about" />

// Relative paths are not supported<Link href="./about" />

You can leverage the useSegments() hooks from expo-router to create complex relative paths. Consider the following structure:

app
(feed)
  _layout.tsx
  feed.tsx
  search.tsx
  profile.tsx
(search)
  profile.tsx
components
button.tsx

You can ensure that you push to the same tab by using the useSegments() hook to get the first segment of the current route.

button.tsx
import { Link, useSegments } from 'expo-router';

export function Button() {
  const [
    // This will be either `(feed)` or `(search)` depending on the current tab.
    first,
  ] = useSegments();

  return <Link href={`/${first}/profile`}>Push profile</Link>;
}

Now, you can leverage <Button /> from both app/(feed)/feed.tsx and app/(search)/search.tsx to push ./profile while preserving the current tab.

Imperative navigation

You can use the typed router object to navigate imperatively:

import { router } from 'expo-router';

router.push('/about');

Or with the typed useRouter() hook:

import { useRouter } from 'expo-router';

function Page() {
  const router = useRouter();

  router.push('/about');

  // ...
}

Query parameters

Most query parameters will not be represented in the file system and therefore cannot be typed automatically. You can type query parameters manually by passing a generic to the useLocalSearchParams and useGlobalSearchParams hooks. For example:

app/search.tsx
import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Page() {
  const { query } = useLocalSearchParams<{ query?: string }>();

  return <Text>Search: {query ?? 'unset'}</Text>;
}

Changes made to the environment

When typed routes is enabled, Expo CLI will generate a git ignored expo-env.d.ts file in your project's root directory, update the .gitignore to ignore the new root expo-env.d.ts file, and modify the tsconfig.json to include the new expo-env.d.ts file.

The includes field in your tsconfig.json gets updated to include expo-env.d.ts and a hidden .expo directory. These entries are required and should not be removed from the file.

The generated expo-env.d.ts should not be removed or changed at any time. It should not be committed and should be ignored by version control.

Global types

Expo CLI will add the following global types to your project when typed routes is enabled:

  • Sets process.env.NODE_ENV = "development" | "production" | "test"
  • Allows the importing of .[css|sass|scss] files
  • Sets the exports of *.module.[css|sass|scss] to be Record<string, string>
  • Add types for Metro's require.context. This is enabled by expo/metro-config and used for static route generation.

React Native Web

With typed routes enabled, Expo CLI also augments the react-native types to support React Native Web. The following changes are made:

  • Add additional web-only styles for ViewStyle, TextStyle, ImageStyle
  • Add tabIndex, aria-level, lang to TextProps
  • Add hovered to Pressable's children and style state callback function
  • Add className elements