Route maps

Use route maps to make $state navigation, route params, resolves, and ng-state examples auditable by TypeScript.

Route maps are optional TypeScript contracts for applications that want route names, params, and resolves checked at author time. They do not change the runtime router; they describe the route contract that the runtime already enforces.

RouterModuleDeclaration is the only public route-registration shape. router() accepts either one declaration or a readonly array of declarations. TDeclaration in its generated signature captures the exact argument literal for inference; it is not another public type. There is no router<TRouteMap>(...) assertion overload and no public state provider. Put an explicit route map on createModule<TRouteMap>(...) when inference is not the desired boundary.

Use StateService<TRoutes> at injection boundaries where code calls $state.go(...) or $state.href(...).

type AdminRoutes = {
  'admin.users': {
    params: {
      page?: number;
      query?: string;
    };
    resolves: {
      users: { id: string }[];
      total: number;
    };
  };
  'admin.profile': {
    params: {
      userId: string;
    };
  };
};

declare const $state: ng.StateService<AdminRoutes>;

const usersParams: ng.ParamsOf<AdminRoutes, 'admin.users'> = {
  page: 1,
  query: 'active',
};

const usersResolves: ng.ResolvesOf<AdminRoutes, 'admin.users'> = {
  users: [{ id: '1' }],
  total: 1,
};

$state.go('admin.users', usersParams);
$state.go('admin.users');
$state.href('admin.profile', { userId: '42' });

With a route map enabled, TypeScript rejects unknown route names, missing required params, omitted required params, extra params, and invalid resolve maps.

// @ts-expect-error unknown route
$state.go('admin.missing', {});

// @ts-expect-error userId is required
$state.href('admin.profile', {});

// @ts-expect-error profile params cannot be omitted
$state.go('admin.profile');

// @ts-expect-error page is not a profile param
$state.go('admin.profile', { page: 1 });

Derive a route map from router(...)

For module-owned route trees, keep the tree literal as the source of truth and derive the public route map from it.

const adminTree = {
  name: 'admin',
  children: [
    {
      name: 'users',
      url: '/users/{page:int}',
      params: { page: { type: 'int' } },
      resolve: { users: () => [{ id: '1' }] },
    },
  ],
} as const satisfies ng.RouterModuleDeclaration;

angular.createModule('admin', []).router(adminTree);
type AdminRoutes = ng.RoutesOf<typeof adminTree>;
declare const $state: ng.StateService<AdminRoutes>;
$state.go('admin.users', { page: 1 });

RoutesOf infers dot-composed child names, absolute child names, explicit params keys, built-in literal URL parameter value types, custom $router.paramTypes value types, and object-style resolve return values.

Pass the same custom param type map used by $router.paramTypes as the second generic argument. decode return types are used first; if a type has no decode, an is(value): value is T predicate is used as the value type.

Large modules can pin the route contract directly on the module. This keeps later route trees and lazy namespaces route-name aware without exposing provider APIs.

type AdminRoutes = ng.RoutesOf<typeof adminTree>;

angular
  .createModule<AdminRoutes>('admin', [])
  .router(adminTree)
  .lazyState('admin.**', () => import('./admin.routes'));

When the tree is declared inline, router(...) infers a ng.RouterModule<ng.RoutesOf<typeof tree>> return type automatically. Typed router modules check the router(...) tree itself, keep route names checked across chained router(...) calls and lazyState(...) prefixes, and narrow object-style params and resolve declarations to the route contract. Array-style resolves remain available when a route needs explicit resolve metadata such as token, deps, or eager.

If a route map is supplied with angular.createModule<Routes>(...), that explicit module contract is preserved after router(...). This lets a feature module declare one route tree while the route map also includes lazy boundaries or states registered later in the same module.

Use that explicit module contract when states must be registered individually:

type AppRoutes = {
  home: {};
  settings: {};
};

angular
  .createModule<AppRoutes>('app', [])
  .router({ name: 'home', url: '/', component: 'homePage' })
  .router({
    name: 'settings',
    url: '/settings',
    component: 'settingsPage',
  });

The route map is declared once on createModule(). Each router() call is then checked against it. Prefer a single inferred tree or forest when the states are available together, because that avoids repeating route names in a handwritten map.

Typed lazy prefixes accept known route names, parent route prefixes, and the explicit ".**" lazy namespace form. Unknown lazy namespaces are rejected when the module has a route map.

Typed router trees accept known route names and parent prefixes. This lets a module declare an abstract or shell parent such as admin while still rejecting unknown concrete children such as admin.audit when they are not in the module route map.

type Uuid = string & { readonly __uuid: unique symbol };

const paramTypes = {
  uuid: {
    pattern: /[0-9a-f-]{36}/i,
    encode(value: Uuid) {
      return value;
    },
    decode(value: string): Uuid {
      return value as Uuid;
    },
  },
};

const appTree = {
  name: 'item',
  url: '/item/{id:uuid}',
} as const;

angular.createModule('items', []).config({ $router: { paramTypes } }).router(appTree);

type ItemRoutes = ng.RoutesOf<typeof appTree, typeof paramTypes>;
declare const $state: ng.StateService<ItemRoutes>;

$state.go('item', { id: 'b4f6eb70-3d2b-42df-8f14-4c7e8e88c208' as Uuid });

Use ng.Transition<TRoutes, { to: ToName; from: FromName }> when code receives an injected $transition$. params() and params('to') read the destination route params; params('from') can be narrowed when the source route is known.

declare const $transition$: ng.Transition<
  AdminRoutes,
  { to: 'admin.profile'; from: 'admin.users' }
>;

const toParams = $transition$.params();
const fromParams = $transition$.params('from');

toParams.userId.toUpperCase();
fromParams.page?.toFixed();

The same route map should be the source of truth for resolves and routed component inputs. At runtime, routed components already receive resolve values by matching binding names. The route map makes that contract visible to TypeScript without asking application code to touch internal view records.

type AdminUsersRoute = AdminRoutes['admin.users'];
type AdminUsersResolves = ng.ResolvesOf<AdminRoutes, 'admin.users'>;

const usersComponent = {
  bindings: {
    users: '<',
    total: '<',
  },
};

const usersRouteContract: AdminUsersResolves = {
  users: [{ id: '1' }],
  total: 1,
};

const checkedUsersRouteContract =
  usersRouteContract satisfies AdminUsersRoute['resolves'];

void usersComponent;
void checkedUsersRouteContract;

Component contracts at the route boundary

Route maps should be mirrored by app-owned component contracts when you want typed, non-internal binding assertions.

type AdminUsersResolves = ng.ResolvesOf<AdminRoutes, 'admin.users'>;

type AdminUsersBindings = {
  users: AdminUsersResolves['users'];
  total: AdminUsersResolves['total'];
};

function buildUsersViewModel(
  routeResolves: AdminUsersResolves,
): AdminUsersBindings {
  return {
    users: routeResolves.users,
    total: routeResolves.total,
  };
}

The example keeps route resolves and component inputs in one named contract so admin.users consumers can evolve in lockstep. This stays explicit and does not require internal router records.

Lazy route map composition

Lazy route declarations are composed per module rather than centralized in one monolithic map.

type AppCoreRoutes = {
  'app.home': {
    params: {};
  };
};

type LazyAdminRoutes = {
  'admin.users': {
    params: { filter?: string };
    resolves: {
      users: { id: string }[];
    };
  };
};

type AppRoutes = AppCoreRoutes & LazyAdminRoutes;
declare const $state: ng.StateService<AppRoutes>;

const adminHref = $state.href('admin.users', { filter: 'active' });
void adminHref;

This pattern keeps typed navigation available at call sites without forcing a root module to own every lazy route declaration.

Generated docs and namespace shape

The generated public surface exposes route maps as helper contracts:

These types are public authoring helpers only. They are not aliases for built state records, transition internals, retained-view cache entries, or view service records. If a route map needs to include a lazy-loaded module, compose the public route-map types at the TypeScript boundary rather than importing router internals.

ng-state always evaluates its value as an expression. Use a quoted literal route name when the target is static; this keeps examples and editor tooling aligned with the same route-map contract. An unquoted identifier is a scope lookup, not an implicit route-name string. Do not wrap ng-state expressions in interpolation braces.

<a ng-state="'admin.profile'" ng-state-params="{ userId: user.id }">
  Profile
</a>

When a template route is static, mirror that literal route name in the component or docs-example TypeScript boundary. This gives TypeScript the same route-map contract without adding a template compiler.

type StaticNgStateLiteral<
  TRoutes extends ng.RouteMap,
  TName extends Extract<keyof TRoutes, string>,
> = {
  state: TName;
  params: ng.ParamsOf<TRoutes, TName>;
};

const profileLink: StaticNgStateLiteral<AdminRoutes, 'admin.profile'> = {
  state: 'admin.profile',
  params: { userId: '42' },
};

Use an identifier or property-path expression only when the route name is genuinely dynamic. The AngularTS VS Code tooling checks simple TypeScript literal unions such as $ctrl.route: 'admin.profile' | 'admin.users'. A broad string cannot prove a finite target set, so use StateService<TRoutes> in the code that computes that value when it needs author-time validation.

Current boundary:

  • $state.go(...), $state.href(...), params, and resolve maps have executable public type coverage.
  • ng.Transition<TRoutes, { to: ToName; from: FromName }> gives injected $transition$ code typed params(), params('to'), and params('from').
  • ng.RouterModule<TRoutes> lets angular.createModule<TRoutes>(...) and router(...) carry route-name checking into module declarations.
  • ng.RoutesOf<typeof tree> derives a route map from a literal router(...) tree without exposing internal state, transition, or view records.
  • Built-in literal URL parameter types and custom $router.paramTypes value types are inferred from route URL patterns.
  • Editor tooling completes and validates literal ng-state route names and ng-state-params keys in inline templates and templateUrl files. It also validates simple dynamic route-name unions when TypeScript can prove them.

Executable checks:

  • src/router/state/state-declaration-types.spec.ts verifies typed route params, resolve maps, $state.go(...), $state.href(...), and $transition$.params(...).
  • state-links.html demonstrates the quoted literal ng-state template shape.