Sunday, March 8, 2020

TypeScript non-null assertion operator

Use non-null assertion operator if and only if you can assert that it will not result to null


If the parameter name passed to paramMap.get has a typo, the paramMap.get will return null. How can the developer really assert that the expression would not return null if the developer can't guarantee that the code has no typo?
product-resolver.service.ts:
@Injectable({
    providedIn: "root"
})
export class ProductResolver implements Resolve<ProductResolved> {
    constructor(private productService: ProductService) {}

    resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<ProductResolved> {
        const id = +route.paramMap.get('id')!;

        // A power user error, or QA-induced error
        if (isNaN(id)) {
            const error = `Product id was not a number: ${id}`;
            console.error(error);

            // false: cancel the route
            // null: continue, the component handle the null object
            // navigate to error page: no built-in resolver mechanism for passing error
            // messages out a route resolver to the prior or activated route.
            return of({ product: null, error });
        }

        return this.productService.getProduct(id).pipe(
            map(product => ({ product })),
            catchError(error => {
                const message = `Retrieval error: ${error}`;
                return of({ product: null, error });
            })
        );
    }
}


Solution:

Since the 'id' parameter on route.paramMap.get is a parameter on Angular Routes, introduce a const for 'id'. This way, making a typo would be impossible, then we can validly assert that it would be impossible to obtain null from paramMap.get since we can only use parameter names on paramMap.get that are present on Angular Routes.


product-resolver.params.ts:
export const PRODUCT_DETAIL_ID_PARAM = "id";
export const PRODUCT_RESOLVED = "productResolved";



product.module.ts:
import {
    PRODUCT_DETAIL_ID_PARAM,
    PRODUCT_RESOLVED
} from "./product-resolver.params";

const ROUTES: Routes = [
    { path: "products", component: ProductListComponent },
    {
        path: `products/:${PRODUCT_DETAIL_ID_PARAM}`,
        resolve: { [PRODUCT_RESOLVED]: ProductResolver },
        component: ProductDetailComponent
    },
    {
        path: `products/:${PRODUCT_DETAIL_ID_PARAM}/edit`,
        resolve: { [PRODUCT_RESOLVED]: ProductResolver },
        component: ProductEditComponent
    }
];

Code smell:
product-resolver.service.ts:
const id = +route.paramMap.get('id')!;


Using literal string (non-const) parameters on paramGet on non-null assertion expression would be a code smell as it is easy to make a typo when passing literal string, the expression will return null if there is a typo, thereby asserting non-null result is at best is weak. Consider any literal string as magic strings. Magic strings can be avoided by using const.

A developer using non-null assertion operator on expression that uses magic strings/numbers is a developer in hubris: "I never make typos in my entire life, promise!"


To remove that code smell on non-null assertion expression on paramMap.get, pass parameter name that was declared from const and Angular Routes:
product-resolver.service.ts:
const id = +route.paramMap.get(PRODUCT_DETAIL_ID_PARAM)!;

No comments:

Post a Comment