Expo UI and Router for iOS
Expo Router

Typed routes

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

Available when using TypeScript in your project. 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:

{
  "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" />

Note: A Route type is also made available by expo-router, which matches all valid routes in a project automatically.

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:

src

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.

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 src/app/(feed)/feed.tsx and src/app/(search)/search.tsx to push ./profile while preserving the current tab.

If you require the segments for a specific use, you can pass the full route to useSegments:

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

export function useMySegments() {
  const segments = useSegments<'/(search)/profile'>();
  //    ^? segments = ['(search)', 'profile']
  return segments;
}

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');

  // ...
}

Route parameters

For strongly typed route parameters, you can pass a full href to the useLocalSearchParams and useGlobalSearchParams hooks. For example:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Page() {
  const {
    profile, // string
    search, // string[]
  } = useLocalSearchParams<'/(search)/[profile]/[...search]'>();

  return (
    <>
      <Text>Profile: {profile}</Text>
      <Text>Search: {search.join(',')}</Text>
    </>
  );
}

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:

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>;
}

If you need a combination of route and query parameters, pass the route as the first generic and then the query parameters:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Page() {
  const { query, profile, search } = useLocalSearchParams<
    '/[profile]/[...search]',
    { 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.

Sources

The original Expo docs for this page:

  • Typed routes (source-of-truth/router/reference/typed-routes.md)

On this page