diff --git a/README.md b/README.md index 782a2fac..944199c6 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ A `RouterStore` service has the following public properties. | `currentRoute$: Observable` | Select the current route. | | `fragment$: Observable` | Select the current route fragment. | | `queryParams$: Observable` | Select the current route query parameters. | -| `routeData$: Observable` | Select the current route data. | +| `routeData$: Observable` | Select the current route data. | | `routeParams$: Observable` | Select the current route parameters. | | `title$: Observable` | Select the resolved route title. | | `url$: Observable` | Select the current URL. | | `selectQueryParam(param: string): Observable` | Select the specified query parameter. | -| `selectRouteData(key: string): Observable` | Select the specified route data. | +| `selectRouteData(key: string): Observable` | Select the specified route data. | | `selectRouteParam(param: string): Observable` | Select the specified route parameter. | | `selectRouterEvents(...acceptedRouterEvents: RouterEvent[]): Observable` | Select router events of the specified router event types. | @@ -169,7 +169,7 @@ The `MinimalActivatedRouteSnapshot` interface is used for the observable `Router | API | Description | | --------------------------------------------------- | ------------------------------------------------ | | `children: MinimalActivatedRouteSnapshot[]` | The children of this route in the route tree. | -| `data: MinimalRouteData` | The static and resolved data of this route. | +| `data: StrictRouteData` | The static and resolved data of this route. | | `firstChild: MinimalActivatedRouteSnapshot \| null` | The first child of this route in the route tree. | | `fragment: string \| null` | The URL fragment shared by all routes. | | `outlet: string` | The outlet name of the route. | @@ -179,12 +179,14 @@ The `MinimalActivatedRouteSnapshot` interface is used for the observable `Router | `title: string \| undefined` | The resolved route title. | | `url: UrlSegment[]` | The URL segments matched by this route. | -#### MinimalRouteData +#### StrictRouteData -The `MinimalRouteData` interface is used for the `RouterStore#data$` property. This interface is a serializable subset of the Angular Router's `Data` type. In particular, the `symbol` index in the Angular Router's `Data` type is removed. `MinimalRouteData` has the following signature. +The `StrictRouteData` interface is used for the `MinimalActivatedRouteSnapshot#data$` and `RouterStore#routeData$` properties. This interface is a serializable subset of the Angular Router's `Data` type. In particular, the `symbol` index in the Angular Router's `Data` type is removed. Additionally, the `any` member type is replaced with `unknown` for stricter typing. + +`StrictRouteData` has the following signature. ```typescript -export type MinimalRouteData = { - [key: string]: any; +export type StrictRouteData = { + [key: string]: unknown; }; ``` diff --git a/packages/router-component-store/src/index.ts b/packages/router-component-store/src/index.ts index 738a7689..64a590fd 100644 --- a/packages/router-component-store/src/index.ts +++ b/packages/router-component-store/src/index.ts @@ -9,4 +9,4 @@ export * from './lib/router-store'; // Serializable route state export * from './lib/@ngrx/router-store/minimal-activated-route-state-snapshot'; -export * from './lib/minimal-route-data'; +export * from './lib/strict-route-data'; diff --git a/packages/router-component-store/src/lib/@ngrx/router-store/minimal-activated-route-state-snapshot.ts b/packages/router-component-store/src/lib/@ngrx/router-store/minimal-activated-route-state-snapshot.ts index e29abb73..8a409ec0 100644 --- a/packages/router-component-store/src/lib/@ngrx/router-store/minimal-activated-route-state-snapshot.ts +++ b/packages/router-component-store/src/lib/@ngrx/router-store/minimal-activated-route-state-snapshot.ts @@ -30,7 +30,7 @@ * found in the LICENSE file at https://angular.io/license */ import { ActivatedRouteSnapshot } from '@angular/router'; -import { MinimalRouteData } from '../../minimal-route-data'; +import { StrictRouteData } from '../../strict-route-data'; /** * Contains the information about a route associated with a component loaded in @@ -67,7 +67,7 @@ export interface MinimalActivatedRouteSnapshot { * the Angular `Router`. Instead, we access the resolved route title through * `MinimalActivatedRouteSnapshot['title']`. */ - readonly data: MinimalRouteData; + readonly data: StrictRouteData; /** * The outlet name of the route. */ diff --git a/packages/router-component-store/src/lib/@ngrx/router-store/minimal_serializer.ts b/packages/router-component-store/src/lib/@ngrx/router-store/minimal_serializer.ts index 70fc6216..5bcdac53 100644 --- a/packages/router-component-store/src/lib/@ngrx/router-store/minimal_serializer.ts +++ b/packages/router-component-store/src/lib/@ngrx/router-store/minimal_serializer.ts @@ -28,7 +28,7 @@ import { Data, RouterStateSnapshot, } from '@angular/router'; -import { MinimalRouteData } from '../../minimal-route-data'; +import { StrictRouteData } from '../../strict-route-data'; import { MinimalActivatedRouteSnapshot } from './minimal-activated-route-state-snapshot'; import { MinimalRouterStateSnapshot } from './minimal-router-state-snapshot'; @@ -43,7 +43,7 @@ export class MinimalRouterStateSerializer { }; } - #serializeRouteData(routeData: Data): MinimalRouteData { + #serializeRouteData(routeData: Data): StrictRouteData { return Object.fromEntries(Object.entries(routeData)); } diff --git a/packages/router-component-store/src/lib/global-router-store/componentless-nested-route-data.spec.ts b/packages/router-component-store/src/lib/global-router-store/componentless-nested-route-data.spec.ts index 4c5814cc..0ee5902d 100644 --- a/packages/router-component-store/src/lib/global-router-store/componentless-nested-route-data.spec.ts +++ b/packages/router-component-store/src/lib/global-router-store/componentless-nested-route-data.spec.ts @@ -1,7 +1,7 @@ import { RouterConfigOptions, Routes } from '@angular/router'; import { firstValueFrom } from 'rxjs'; -import { MinimalRouteData } from '../minimal-route-data'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; import { GlobalRouterStore } from './global-router-store'; import { globalRouterStoreSetup } from './test-util/global-router-store-setup'; import { @@ -103,7 +103,7 @@ describe(`${GlobalRouterStore.name} componentless nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { componentlessBeforeParent: 'componentless-route-data-before-parent', parent: 'parent-route-data', @@ -167,7 +167,7 @@ describe(`${GlobalRouterStore.name} componentless nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { componentlessBeforeParent: 'componentless-route-data-before-parent', parent: 'parent-route-data', @@ -218,7 +218,7 @@ describe(`${GlobalRouterStore.name} componentless nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { componentlessBeforeParent: 'componentless-route-data-before-parent', parent: 'parent-route-data', componentlessBeforeChild: 'componentless-route-data-before-child', diff --git a/packages/router-component-store/src/lib/global-router-store/global-router-store.ts b/packages/router-component-store/src/lib/global-router-store/global-router-store.ts index 4bfcba08..583ffbb6 100644 --- a/packages/router-component-store/src/lib/global-router-store/global-router-store.ts +++ b/packages/router-component-store/src/lib/global-router-store/global-router-store.ts @@ -1,6 +1,5 @@ import { inject, Injectable, Type } from '@angular/core'; import { - Data, Event as RouterEvent, NavigationCancel, NavigationEnd, @@ -17,6 +16,7 @@ import { MinimalRouterStateSnapshot } from '../@ngrx/router-store/minimal-router import { MinimalRouterStateSerializer } from '../@ngrx/router-store/minimal_serializer'; import { filterRouterEvents } from '../filter-router-event.operator'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; interface GlobalRouterState { readonly routerState: MinimalRouterStateSnapshot; @@ -56,7 +56,7 @@ export class GlobalRouterStore this.#rootRoute$, (route) => route.queryParams ); - routeData$: Observable = this.select( + routeData$: Observable = this.select( this.currentRoute$, (route) => route.data ); @@ -105,7 +105,7 @@ export class GlobalRouterStore return this.select(this.queryParams$, (params) => params[param]); } - selectRouteData(key: string): Observable { + selectRouteData(key: string): Observable { return this.select(this.routeData$, (data) => data[key]); } diff --git a/packages/router-component-store/src/lib/global-router-store/nested-route-data.spec.ts b/packages/router-component-store/src/lib/global-router-store/nested-route-data.spec.ts index 2854d67d..0c480c42 100644 --- a/packages/router-component-store/src/lib/global-router-store/nested-route-data.spec.ts +++ b/packages/router-component-store/src/lib/global-router-store/nested-route-data.spec.ts @@ -1,7 +1,7 @@ import { Routes } from '@angular/router'; import { firstValueFrom } from 'rxjs'; -import { MinimalRouteData } from '../minimal-route-data'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; import { GlobalRouterStore } from './global-router-store'; import { globalRouterStoreSetup } from './test-util/global-router-store-setup'; import { @@ -63,7 +63,7 @@ describe(`${GlobalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { grandchild: 'grandchild-route-data', shadowed: 'grandchild-route-data', }; @@ -104,7 +104,7 @@ describe(`${GlobalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { child: 'child-route-data', shadowed: 'child-route-data', }; @@ -136,7 +136,7 @@ describe(`${GlobalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', shadowed: 'parent-route-data', }; @@ -181,7 +181,7 @@ describe(`${GlobalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', child: 'child-route-data', grandchild: 'grandchild-route-data', @@ -229,7 +229,7 @@ describe(`${GlobalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', child: 'child-route-data', shadowed: 'child-route-data', @@ -264,7 +264,7 @@ describe(`${GlobalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', shadowed: 'parent-route-data', }; diff --git a/packages/router-component-store/src/lib/global-router-store/selectors.spec.ts b/packages/router-component-store/src/lib/global-router-store/selectors.spec.ts index ec568728..91fd41a1 100644 --- a/packages/router-component-store/src/lib/global-router-store/selectors.spec.ts +++ b/packages/router-component-store/src/lib/global-router-store/selectors.spec.ts @@ -17,8 +17,8 @@ import { provideStore, Store } from '@ngrx/store'; import { createFeatureHarness } from '@ngworker/spectacular'; import { filter, firstValueFrom, take, toArray } from 'rxjs'; import { MinimalActivatedRouteSnapshot } from '../@ngrx/router-store/minimal-activated-route-state-snapshot'; -import { MinimalRouteData } from '../minimal-route-data'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; import { GlobalRouterStore } from './global-router-store'; import { provideGlobalRouterStore } from './provide-global-router-store'; @@ -215,7 +215,7 @@ describe(`${GlobalRouterStore.name} selectors`, () => { }); it('exposes a selector for route data', async () => { - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { testData: 'test-data', }; const { harness, ngrxRouterStore } = setup({ @@ -249,9 +249,7 @@ describe(`${GlobalRouterStore.name} selectors`, () => { ); await expect( - firstValueFrom( - harness.inject(RouterStore).selectRouteData('testData') - ) + firstValueFrom(harness.inject(RouterStore).selectRouteData('testData')) ).resolves.toBe(expectedTestData); await expect( firstValueFrom( diff --git a/packages/router-component-store/src/lib/local-router-store/componentless-nested-route-data.spec.ts b/packages/router-component-store/src/lib/local-router-store/componentless-nested-route-data.spec.ts index 9ef5586f..d4de686f 100644 --- a/packages/router-component-store/src/lib/local-router-store/componentless-nested-route-data.spec.ts +++ b/packages/router-component-store/src/lib/local-router-store/componentless-nested-route-data.spec.ts @@ -1,7 +1,7 @@ import { RouterConfigOptions, Routes } from '@angular/router'; import { firstValueFrom } from 'rxjs'; -import { MinimalRouteData } from '../minimal-route-data'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; import { LocalRouterStore } from './local-router-store'; import { localRouterStoreSetup } from './test-util/local-router-store-setup'; import { @@ -93,7 +93,7 @@ describe(`${LocalRouterStore.name} componentless nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { componentlessBeforeParent: 'componentless-route-data-before-parent', parent: 'parent-route-data', @@ -136,7 +136,7 @@ describe(`${LocalRouterStore.name} componentless nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { componentlessBeforeParent: 'componentless-route-data-before-parent', parent: 'parent-route-data', @@ -185,7 +185,7 @@ describe(`${LocalRouterStore.name} componentless nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { componentlessBeforeParent: 'componentless-route-data-before-parent', parent: 'parent-route-data', componentlessBeforeChild: 'componentless-route-data-before-child', diff --git a/packages/router-component-store/src/lib/local-router-store/local-router-store.ts b/packages/router-component-store/src/lib/local-router-store/local-router-store.ts index 3ba340c2..a83d9592 100644 --- a/packages/router-component-store/src/lib/local-router-store/local-router-store.ts +++ b/packages/router-component-store/src/lib/local-router-store/local-router-store.ts @@ -2,7 +2,6 @@ import { inject, Injectable, Type } from '@angular/core'; import { ActivatedRoute, createUrlTreeFromSnapshot, - Data, Event as RouterEvent, NavigationCancel, NavigationEnd, @@ -20,6 +19,7 @@ import { MinimalRouterStateSnapshot } from '../@ngrx/router-store/minimal-router import { MinimalRouterStateSerializer } from '../@ngrx/router-store/minimal_serializer'; import { filterRouterEvents } from '../filter-router-event.operator'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; interface LocalRouterState { readonly routerState: MinimalRouterStateSnapshot; @@ -45,7 +45,7 @@ export class LocalRouterStore currentRoute$: Observable = this.#localRoute; fragment$: Observable; queryParams$: Observable; - routeData$: Observable; + routeData$: Observable; routeParams$: Observable; title$: Observable; url$: Observable = this.select( @@ -91,7 +91,7 @@ export class LocalRouterStore return this.select(this.queryParams$, (params) => params[param]); } - selectRouteData(key: string): Observable { + selectRouteData(key: string): Observable { return this.select(this.routeData$, (data) => data[key]); } diff --git a/packages/router-component-store/src/lib/local-router-store/nested-route-data.spec.ts b/packages/router-component-store/src/lib/local-router-store/nested-route-data.spec.ts index a246cff0..c068e8c3 100644 --- a/packages/router-component-store/src/lib/local-router-store/nested-route-data.spec.ts +++ b/packages/router-component-store/src/lib/local-router-store/nested-route-data.spec.ts @@ -1,7 +1,7 @@ import { RouterConfigOptions, Routes } from '@angular/router'; import { firstValueFrom } from 'rxjs'; -import { MinimalRouteData } from '../minimal-route-data'; import { RouterStore } from '../router-store'; +import { StrictRouteData } from '../strict-route-data'; import { LocalRouterStore } from './local-router-store'; import { localRouterStoreSetup } from './test-util/local-router-store-setup'; import { @@ -63,7 +63,7 @@ describe(`${LocalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', shadowed: 'parent-route-data', }; @@ -100,7 +100,7 @@ describe(`${LocalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { child: 'child-route-data', shadowed: 'child-route-data', }; @@ -132,7 +132,7 @@ describe(`${LocalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { grandchild: 'grandchild-route-data', shadowed: 'grandchild-route-data', }; @@ -170,7 +170,7 @@ describe(`${LocalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', child: 'child-route-data', shadowed: 'child-route-data', @@ -208,7 +208,7 @@ describe(`${LocalRouterStore.name} nested route data`, () => { routes, }); - const expectedRouteData: MinimalRouteData = { + const expectedRouteData: StrictRouteData = { parent: 'parent-route-data', child: 'child-route-data', grandchild: 'grandchild-route-data', diff --git a/packages/router-component-store/src/lib/local-router-store/selectors.spec.ts b/packages/router-component-store/src/lib/local-router-store/selectors.spec.ts index 77e2fe18..bd213732 100644 --- a/packages/router-component-store/src/lib/local-router-store/selectors.spec.ts +++ b/packages/router-component-store/src/lib/local-router-store/selectors.spec.ts @@ -289,7 +289,7 @@ describe(`${LocalRouterStore.name} selectors`, () => { firstValueFrom( injectorFor(DummyAuthComponent) .get(RouterStore) - .selectRouteData('testData') + .selectRouteData('testData') ) ).resolves.toBe('test-data'); }); diff --git a/packages/router-component-store/src/lib/router-store.ts b/packages/router-component-store/src/lib/router-store.ts index 0be529c5..91c45ae9 100644 --- a/packages/router-component-store/src/lib/router-store.ts +++ b/packages/router-component-store/src/lib/router-store.ts @@ -1,7 +1,8 @@ import { Injectable, Type } from '@angular/core'; -import { Data, Event as RouterEvent, Params } from '@angular/router'; +import { Event as RouterEvent, Params } from '@angular/router'; import { Observable } from 'rxjs'; import { MinimalActivatedRouteSnapshot } from './@ngrx/router-store/minimal-activated-route-state-snapshot'; +import { StrictRouteData } from './strict-route-data'; /** * An Angular Router-connecting NgRx component store. @@ -48,7 +49,7 @@ export abstract class RouterStore { /** * Select the current route data. */ - abstract readonly routeData$: Observable; + abstract readonly routeData$: Observable; /** * Select the current route parameters. */ @@ -67,9 +68,9 @@ export abstract class RouterStore { * @param key The route data key. * * @example Usage - * const limit$ = routerStore.selectRouteData('limit'); + * const limit$ = routerStore.selectRouteData('limit').pipe(map(x => Number(x))); */ - abstract selectRouteData(key: string): Observable; + abstract selectRouteData(key: string): Observable; /** * Select the specified query parameter. * diff --git a/packages/router-component-store/src/lib/minimal-route-data.ts b/packages/router-component-store/src/lib/strict-route-data.ts similarity index 59% rename from packages/router-component-store/src/lib/minimal-route-data.ts rename to packages/router-component-store/src/lib/strict-route-data.ts index 11fb9d9e..c38508f7 100644 --- a/packages/router-component-store/src/lib/minimal-route-data.ts +++ b/packages/router-component-store/src/lib/strict-route-data.ts @@ -1,9 +1,12 @@ import { Data } from '@angular/router'; import { OmitSymbolIndex } from './util-types/omit-symbol-index'; +import { StrictNoAny } from './util-types/strict-no-any'; /** * Serializable route `Data` without its symbol index, in particular without the * `Symbol(RouteTitle)` key as this is an internal value for the Angular * `Router`. + * + * Additionally, the `any` member type is converted to `unknown`. */ -export type MinimalRouteData = OmitSymbolIndex; +export type StrictRouteData = StrictNoAny>; diff --git a/packages/router-component-store/src/lib/util-types/omit-symbol-index.ts b/packages/router-component-store/src/lib/util-types/omit-symbol-index.ts index 137a414a..c1f9d70d 100644 --- a/packages/router-component-store/src/lib/util-types/omit-symbol-index.ts +++ b/packages/router-component-store/src/lib/util-types/omit-symbol-index.ts @@ -4,10 +4,10 @@ * @example Usage * ``` * type RouteData = { [key: string | symbol]: any; }; - * type MinimalRouteData = OmitSymbolIndex; + * type SerializableRouteData = OmitSymbolIndex; * ``` * - * `MinimalRouteData` is `{ [key: string]: any }`. + * `SerializableRouteData` is `{ [key: string]: any }`. */ export type OmitSymbolIndex = { [TShapeKey in keyof TShape as symbol extends TShapeKey diff --git a/packages/router-component-store/src/lib/util-types/strict-no-any.ts b/packages/router-component-store/src/lib/util-types/strict-no-any.ts new file mode 100644 index 00000000..56de5798 --- /dev/null +++ b/packages/router-component-store/src/lib/util-types/strict-no-any.ts @@ -0,0 +1,18 @@ +/** + * Convert `any` member types to `unknown` in the specified type. + * + * @example Usage + * ``` + * type RouteData = { [key: string | symbol]: any; }; + * type StrictRouteData = Strict; + * ``` + * + * `StrictRouteData` is `{ [key: string | symbol]: unknown }`. + */ +export type StrictNoAny = { + // [@typescript-eslint/no-explicit-any] We detect `any` to convert it to `unknown` + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [TShapeKey in keyof TShape]: TShape[TShapeKey] extends any + ? unknown + : TShape[TShapeKey]; +};