Skip to content

Typed Navigation

Type-safe programmatic navigation utilities for client-side and server-side navigation with compile-time route validation.

Import

ts
import {
  typedNavigate,
  typedRedirect,
  redirect,           // Alias for typedRedirect
  useTypedNavigate,
  buildTypedUrl,
  parseTypedSearchParams,
  parseTypedHashParams,
  preloadRoute,
  goBack,
  goForward,
  go,
  isCurrentPath,
} from '@ereo/client'

// Types
import type {
  TypedNavigateOptions,
  TypedRedirectOptions,
  TypedNavigateFunction,
} from '@ereo/client'

typedNavigate

Type-safe client-side navigation function.

Signature

ts
async function typedNavigate<Path extends TypedRoutes>(
  path: Path,
  options?: TypedNavigateOptions<Path>
): Promise<void>

Options

ts
type TypedNavigateOptions<Path> = {
  // Route params - required if route has params
  params?: RouteParamsFor<Path>

  // Search params - typed per route
  search?: Partial<SearchParamsFor<Path>>

  // Hash params - typed per route (Ereo exclusive)
  hash?: Partial<HashParamsFor<Path>>

  // Replace history instead of push
  replace?: boolean

  // State to pass to the location
  state?: unknown

  // Scroll to top after navigation (default: true)
  scroll?: boolean
}

Examples

ts
import { typedNavigate } from '@ereo/client'

// Navigate to a static route
await typedNavigate('/about')

// Navigate with params
await typedNavigate('/users/[id]', {
  params: { id: '123' },
})

// Navigate with search and hash params
await typedNavigate('/posts', {
  search: { page: 2, sort: 'newest' },
  hash: { section: 'comments' },
})

// Replace history entry
await typedNavigate('/step-2', { replace: true })

// Pass state to destination
await typedNavigate('/checkout', {
  state: { from: 'cart', itemCount: 3 },
})

// Prevent scroll reset
await typedNavigate('/posts/[slug]', {
  params: { slug: 'hello' },
  scroll: false,
})

Compile-Time Validation

ts
// Error: Route '/invalid' does not exist
await typedNavigate('/invalid')

// Error: Property 'id' is missing
await typedNavigate('/users/[id]')

// Error: Type 'number' is not assignable to type 'string'
await typedNavigate('/users/[id]', { params: { id: 123 } })

useTypedNavigate

Hook that returns a type-safe navigate function for use in components.

Signature

ts
function useTypedNavigate(): TypedNavigateFunction

interface TypedNavigateFunction {
  // Navigate to a typed route
  <Path extends TypedRoutes>(
    path: Path,
    options?: TypedNavigateOptions<Path>
  ): Promise<void>

  // Navigate by delta (back/forward)
  (delta: number): void
}

Example

tsx
import { useTypedNavigate } from '@ereo/client'

function UserActions({ userId }: { userId: string }) {
  const navigate = useTypedNavigate()

  const handleView = () => {
    navigate('/users/[id]', { params: { id: userId } })
  }

  const handleEdit = () => {
    navigate('/users/[id]/edit', {
      params: { id: userId },
      state: { returnTo: '/users' },
    })
  }

  const handleBack = () => {
    navigate(-1)
  }

  return (
    <div>
      <button onClick={handleView}>View</button>
      <button onClick={handleEdit}>Edit</button>
      <button onClick={handleBack}>Back</button>
    </div>
  )
}

typedRedirect

Type-safe server-side redirect that returns a Response object.

Signature

ts
function typedRedirect<Path extends TypedRoutes>(
  path: Path,
  options?: TypedRedirectOptions<Path>
): Response

Options

ts
type TypedRedirectOptions<Path> = {
  // Route params - required if route has params
  params?: RouteParamsFor<Path>

  // Search params
  search?: Partial<SearchParamsFor<Path>>

  // Hash params (Ereo exclusive)
  hash?: Partial<HashParamsFor<Path>>

  // HTTP status code (default: 302)
  status?: 301 | 302 | 303 | 307 | 308

  // Custom headers
  headers?: HeadersInit
}

Examples

ts
import { typedRedirect } from '@ereo/client'

// In a loader - redirect to login
export const loader = async ({ request, context }) => {
  const user = context.get('user')

  if (!user) {
    return typedRedirect('/login', {
      search: { returnTo: new URL(request.url).pathname },
    })
  }

  return { user }
}

// Redirect to a dynamic route
return typedRedirect('/users/[id]', {
  params: { id: user.id },
})

// Permanent redirect
return typedRedirect('/new-location', {
  status: 301,
})

// Redirect after POST (303 See Other)
return typedRedirect('/success', {
  status: 303,
})

// Redirect with custom headers
return typedRedirect('/dashboard', {
  headers: {
    'X-Redirect-Reason': 'post-action',
  },
})

Status Code Reference

StatusNameUse Case
301Moved PermanentlyPermanent URL change (SEO redirect)
302FoundTemporary redirect (default)
303See OtherRedirect after POST to prevent resubmit
307Temporary RedirectPreserve request method
308Permanent RedirectLike 301, but preserve method

buildTypedUrl

Build URLs programmatically with type safety.

Signature

ts
function buildTypedUrl<Path extends TypedRoutes>(
  pattern: Path,
  options?: {
    params?: RouteParamsFor<Path>
    search?: Partial<SearchParamsFor<Path>>
    hash?: Partial<HashParamsFor<Path>>
  }
): string

Examples

ts
import { buildTypedUrl } from '@ereo/client'

// Simple static route
const url1 = buildTypedUrl('/about')
// '/about'

// With params
const url2 = buildTypedUrl('/users/[id]', {
  params: { id: '123' },
})
// '/users/123'

// With multiple params
const url3 = buildTypedUrl('/users/[id]/posts/[postId]', {
  params: { id: '123', postId: '456' },
})
// '/users/123/posts/456'

// With search params
const url4 = buildTypedUrl('/posts', {
  search: { page: 2, sort: 'newest' },
})
// '/posts?page=2&sort=newest'

// With hash params (Ereo exclusive)
const url5 = buildTypedUrl('/docs/[topic]', {
  params: { topic: 'routing' },
  hash: { section: 'params' },
})
// '/docs/routing#section=params'

// Full example
const url6 = buildTypedUrl('/posts/[slug]', {
  params: { slug: 'hello-world' },
  search: { showComments: true },
  hash: { comment: 'c123' },
})
// '/posts/hello-world?showComments=true#comment=c123'

URL Parsing

parseTypedSearchParams

Parse search params from a URL with type safety.

ts
function parseTypedSearchParams<Path extends TypedRoutes>(
  url: URL | string
): Partial<SearchParamsFor<Path>>
ts
import { parseTypedSearchParams } from '@ereo/client'

const url = new URL('https://example.com/posts?page=2&sort=newest')
const searchParams = parseTypedSearchParams<'/posts'>(url)
// { page: '2', sort: 'newest' }

parseTypedHashParams (Ereo Exclusive)

Parse hash params from a URL with type safety. This feature is unique to Ereo.

ts
function parseTypedHashParams<Path extends TypedRoutes>(
  url: URL | string
): Partial<HashParamsFor<Path>>
ts
import { parseTypedHashParams } from '@ereo/client'

const url = new URL('https://example.com/docs/routing#section=params&line=42')
const hashParams = parseTypedHashParams<'/docs/[topic]'>(url)
// { section: 'params', line: '42' }

Preloading

preloadRoute

Preload a route's data before navigation for faster transitions.

ts
async function preloadRoute<Path extends TypedRoutes>(
  path: Path,
  options?: {
    params?: RouteParamsFor<Path>
    search?: Partial<SearchParamsFor<Path>>
  }
): Promise<void>
tsx
import { preloadRoute } from '@ereo/client'

function UserCard({ userId }: { userId: string }) {
  return (
    <div
      onMouseEnter={() => {
        // Preload on hover
        preloadRoute('/users/[id]', { params: { id: userId } })
      }}
    >
      <TypedLink to="/users/[id]" params={{ id: userId }}>
        View Profile
      </TypedLink>
    </div>
  )
}

History Utilities

goBack

Navigate back in history.

ts
import { goBack } from '@ereo/client'

function BackButton() {
  return <button onClick={goBack}>Go Back</button>
}

goForward

Navigate forward in history.

ts
import { goForward } from '@ereo/client'

function ForwardButton() {
  return <button onClick={goForward}>Go Forward</button>
}

go

Navigate to a specific history entry.

ts
import { go } from '@ereo/client'

// Go back 2 entries
go(-2)

// Go forward 1 entry
go(1)

isCurrentPath

Check if a path matches the current location.

ts
function isCurrentPath<Path extends TypedRoutes>(
  path: Path,
  options?: {
    params?: RouteParamsFor<Path>
    exact?: boolean
  }
): boolean
tsx
import { isCurrentPath } from '@ereo/client'

function Sidebar() {
  const isUsersSection = isCurrentPath('/users')
  const isSpecificUser = isCurrentPath('/users/[id]', {
    params: { id: '123' },
    exact: true,
  })

  return (
    <nav>
      <span className={isUsersSection ? 'active' : ''}>Users</span>
    </nav>
  )
}

Patterns

Protected Route Redirect

ts
export const loader = async ({ request, context }) => {
  const user = await context.get<User>('user')

  if (!user) {
    return typedRedirect('/login', {
      search: { returnTo: new URL(request.url).pathname },
    })
  }

  if (!user.isAdmin) {
    return typedRedirect('/unauthorized')
  }

  return { user }
}

Wizard Navigation

tsx
function WizardStep({ step, data }: { step: number; data: FormData }) {
  const navigate = useTypedNavigate()

  const handleNext = () => {
    navigate('/wizard/[step]', {
      params: { step: String(step + 1) },
      state: { data },
      replace: true,
    })
  }

  const handleBack = () => {
    navigate('/wizard/[step]', {
      params: { step: String(step - 1) },
      state: { data },
      replace: true,
    })
  }

  return (
    <div>
      {step > 1 && <button onClick={handleBack}>Previous</button>}
      <button onClick={handleNext}>Next</button>
    </div>
  )
}

Search Filter Navigation

tsx
function Filters({ currentFilters }) {
  const navigate = useTypedNavigate()

  const updateFilter = (key: string, value: string) => {
    navigate('/products', {
      search: { ...currentFilters, [key]: value, page: 1 },
      replace: true,
    })
  }

  return (
    <select onChange={(e) => updateFilter('category', e.target.value)}>
      <option value="">All Categories</option>
      <option value="electronics">Electronics</option>
      <option value="clothing">Clothing</option>
    </select>
  )
}

Deep Linking with Hash

tsx
function TableOfContents({ sections }) {
  return (
    <nav>
      {sections.map((section) => (
        <TypedLink
          key={section.id}
          to="/docs/[topic]"
          params={{ topic: 'current' }}
          hash={{ section: section.id }}
        >
          {section.title}
        </TypedLink>
      ))}
    </nav>
  )
}

TanStack Start Comparison

FeatureTanStack StartEreoJS
Type-safe navigatePartialFull
Search params typedLimitedFull
Hash params typedNoYes
Preload route dataYesYes
Server redirectYesYes
History utilitiesYesYes

Released under the MIT License.