Skip to content

Commit

Permalink
feat(router): Add reusable types for router guards (#54580)
Browse files Browse the repository at this point in the history
This refactor makes it easier to update the return types of guards.
Rather than having to track what types guards can return, which may
change with new features over time, `MaybeAsync<GuardResult>` can be
used instead.

PR Close #54580
  • Loading branch information
atscott authored and pkozlowski-opensource committed Feb 28, 2024
1 parent fb540e1 commit c1c7384
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 65 deletions.
30 changes: 18 additions & 12 deletions goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,47 +115,47 @@ export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
// @public @deprecated
export interface CanActivate {
// (undocumented)
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): MaybeAsync<GuardResult>;
}

// @public @deprecated
export interface CanActivateChild {
// (undocumented)
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): MaybeAsync<GuardResult>;
}

// @public
export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<GuardResult>;

// @public
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<GuardResult>;

// @public @deprecated
export interface CanDeactivate<T> {
// (undocumented)
canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): MaybeAsync<GuardResult>;
}

// @public
export type CanDeactivateFn<T> = (component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanDeactivateFn<T> = (component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => MaybeAsync<GuardResult>;

// @public @deprecated
export interface CanLoad {
// (undocumented)
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canLoad(route: Route, segments: UrlSegment[]): MaybeAsync<GuardResult>;
}

// @public @deprecated
export type CanLoadFn = (route: Route, segments: UrlSegment[]) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanLoadFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<GuardResult>;

// @public @deprecated
export interface CanMatch {
// (undocumented)
canMatch(route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canMatch(route: Route, segments: UrlSegment[]): MaybeAsync<GuardResult>;
}

// @public
export type CanMatchFn = (route: Route, segments: UrlSegment[]) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanMatchFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<GuardResult>;

// @public
export class ChildActivationEnd {
Expand Down Expand Up @@ -305,6 +305,9 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti
useHash?: boolean;
}

// @public
export type GuardResult = boolean | UrlTree;

// @public
export class GuardsCheckEnd extends RouterEvent {
constructor(
Expand Down Expand Up @@ -396,6 +399,9 @@ export function mapToResolve<T>(provider: Type<{
resolve: ResolveFn<T>;
}>): ResolveFn<T>;

// @public
export type MaybeAsync<T> = T | Observable<T> | Promise<T>;

// @public
export interface Navigation {
extractedUrl: UrlTree;
Expand Down Expand Up @@ -592,7 +598,7 @@ export type QueryParamsHandling = 'merge' | 'preserve' | '';
// @public @deprecated
export interface Resolve<T> {
// (undocumented)
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T;
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): MaybeAsync<T>;
}

// @public
Expand All @@ -618,7 +624,7 @@ export class ResolveEnd extends RouterEvent {
}

// @public
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<T> | Promise<T> | T;
export type ResolveFn<T> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => MaybeAsync<T>;

// @public
export class ResolveStart extends RouterEvent {
Expand Down
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export {
} from './events';
export {
CanActivateChildFn,
MaybeAsync,
GuardResult,
CanActivateFn,
CanDeactivateFn,
CanLoadFn,
Expand Down
63 changes: 30 additions & 33 deletions packages/router/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ export type OnSameUrlNavigation = 'reload' | 'ignore';
*/
export type DeprecatedGuard = ProviderToken<any> | any;

/**
* The supported types that can be returned from a `Router` guard.
*
* @see [Routing tutorial](guide/router-tutorial-toh#milestone-5-route-guards)
* @publicApi
*/
export type GuardResult = boolean | UrlTree;

/**
* Type used to represent a value which may be synchronous or async.
*
* @publicApi
*/
export type MaybeAsync<T> = T | Observable<T> | Promise<T>;

/**
* Represents a route configuration for the Router service.
* An array of `Route` objects, used in `Router.config` and for nested route configurations
Expand Down Expand Up @@ -693,7 +708,7 @@ export interface LoadedRouterConfig {
* canActivate(
* route: ActivatedRouteSnapshot,
* state: RouterStateSnapshot
* ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
* ): MaybeAsync<GuardResult> {
* return this.permissions.canActivate(this.currentUser, route.params.id);
* }
* }
Expand Down Expand Up @@ -725,10 +740,7 @@ export interface LoadedRouterConfig {
* @see {@link CanActivateFn}
*/
export interface CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): MaybeAsync<GuardResult>;
}

/**
Expand All @@ -754,7 +766,7 @@ export interface CanActivate {
export type CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
) => MaybeAsync<GuardResult>;

/**
* @description
Expand Down Expand Up @@ -782,7 +794,7 @@ export type CanActivateFn = (
* canActivateChild(
* route: ActivatedRouteSnapshot,
* state: RouterStateSnapshot
* ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
* ): MaybeAsync<GuardResult> {
* return this.permissions.canActivate(this.currentUser, route.params.id);
* }
* }
Expand Down Expand Up @@ -822,7 +834,7 @@ export interface CanActivateChild {
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
): MaybeAsync<GuardResult>;
}

/**
Expand All @@ -843,7 +855,7 @@ export interface CanActivateChild {
export type CanActivateChildFn = (
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
) => MaybeAsync<GuardResult>;

/**
* @description
Expand Down Expand Up @@ -879,7 +891,7 @@ export type CanActivateChildFn = (
* currentRoute: ActivatedRouteSnapshot,
* currentState: RouterStateSnapshot,
* nextState: RouterStateSnapshot
* ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
* ): MaybeAsync<GuardResult> {
* return this.permissions.canDeactivate(this.currentUser, route.params.id);
* }
* }
Expand Down Expand Up @@ -911,7 +923,7 @@ export interface CanDeactivate<T> {
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
): MaybeAsync<GuardResult>;
}

/**
Expand All @@ -934,7 +946,7 @@ export type CanDeactivateFn<T> = (
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot,
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
) => MaybeAsync<GuardResult>;

/**
* @description
Expand Down Expand Up @@ -1002,10 +1014,7 @@ export type CanDeactivateFn<T> = (
* @see {@link CanMatchFn}
*/
export interface CanMatch {
canMatch(
route: Route,
segments: UrlSegment[],
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canMatch(route: Route, segments: UrlSegment[]): MaybeAsync<GuardResult>;
}

/**
Expand All @@ -1023,10 +1032,7 @@ export interface CanMatch {
* @publicApi
* @see {@link Route}
*/
export type CanMatchFn = (
route: Route,
segments: UrlSegment[],
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanMatchFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<GuardResult>;

/**
* @description
Expand Down Expand Up @@ -1126,10 +1132,7 @@ export type CanMatchFn = (
* @see {@link ResolveFn}
*/
export interface Resolve<T> {
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<T> | Promise<T> | T;
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): MaybeAsync<T>;
}

/**
Expand Down Expand Up @@ -1176,7 +1179,7 @@ export interface Resolve<T> {
export type ResolveFn<T> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => Observable<T> | Promise<T> | T;
) => MaybeAsync<T>;

/**
* @description
Expand Down Expand Up @@ -1233,10 +1236,7 @@ export type ResolveFn<T> = (
* @deprecated Use {@link CanMatchFn} instead
*/
export interface CanLoad {
canLoad(
route: Route,
segments: UrlSegment[],
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
canLoad(route: Route, segments: UrlSegment[]): MaybeAsync<GuardResult>;
}

/**
Expand All @@ -1248,10 +1248,7 @@ export interface CanLoad {
* @see {@link CanMatchFn}
* @deprecated Use `Route.canMatch` and `CanMatchFn` instead
*/
export type CanLoadFn = (
route: Route,
segments: UrlSegment[],
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
export type CanLoadFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<GuardResult>;

/**
* @description
Expand Down
25 changes: 10 additions & 15 deletions packages/router/src/operators/check_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
CanActivateChildFn,
CanActivateFn,
CanDeactivateFn,
GuardResult,
CanLoadFn,
CanMatchFn,
Route,
Expand Down Expand Up @@ -91,12 +92,9 @@ function runCanDeactivateChecks(
mergeMap((check) =>
runCanDeactivate(check.component, check.route, currRSS, futureRSS, injector),
),
first(
(result) => {
return result !== true;
},
true as boolean | UrlTree,
),
first((result) => {
return result !== true;
}, true),
);
}

Expand All @@ -115,12 +113,9 @@ function runCanActivateChecks(
runCanActivate(futureSnapshot, check.route, injector),
);
}),
first(
(result) => {
return result !== true;
},
true as boolean | UrlTree,
),
first((result) => {
return result !== true;
}, true),
);
}

Expand Down Expand Up @@ -164,7 +159,7 @@ function runCanActivate(
futureRSS: RouterStateSnapshot,
futureARS: ActivatedRouteSnapshot,
injector: EnvironmentInjector,
): Observable<boolean | UrlTree> {
): Observable<GuardResult> {
const canActivate = futureARS.routeConfig ? futureARS.routeConfig.canActivate : null;
if (!canActivate || canActivate.length === 0) return of(true);

Expand All @@ -189,7 +184,7 @@ function runCanActivateChild(
futureRSS: RouterStateSnapshot,
path: ActivatedRouteSnapshot[],
injector: EnvironmentInjector,
): Observable<boolean | UrlTree> {
): Observable<GuardResult> {
const futureARS = path[path.length - 1];

const canActivateChildGuards = path
Expand Down Expand Up @@ -227,7 +222,7 @@ function runCanDeactivate(
currRSS: RouterStateSnapshot,
futureRSS: RouterStateSnapshot,
injector: EnvironmentInjector,
): Observable<boolean | UrlTree> {
): Observable<GuardResult> {
const canDeactivate = currARS && currARS.routeConfig ? currARS.routeConfig.canDeactivate : null;
if (!canDeactivate || canDeactivate.length === 0) return of(true);
const canDeactivateObservables = canDeactivate.map((c: any) => {
Expand Down
8 changes: 3 additions & 5 deletions packages/router/src/operators/prioritized_guard_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@
import {combineLatest, Observable, OperatorFunction} from 'rxjs';
import {filter, map, startWith, switchMap, take} from 'rxjs/operators';

import {GuardResult} from '../models';
import {UrlTree} from '../url_tree';

const INITIAL_VALUE = /* @__PURE__ */ Symbol('INITIAL_VALUE');
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree;

export function prioritizedGuardValue(): OperatorFunction<
Observable<boolean | UrlTree>[],
boolean | UrlTree
> {
export function prioritizedGuardValue(): OperatorFunction<Observable<GuardResult>[], GuardResult> {
return switchMap((obs) => {
return combineLatest(
obs.map((o) => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES))),
Expand All @@ -40,7 +38,7 @@ export function prioritizedGuardValue(): OperatorFunction<
// Everything resolved to true. Return true.
return true;
}),
filter((item): item is boolean | UrlTree => item !== INITIAL_VALUE),
filter((item): item is GuardResult => item !== INITIAL_VALUE),
take(1),
);
});
Expand Down

0 comments on commit c1c7384

Please sign in to comment.