React Router's lazy type handling and overcoming the impact with type-safe solutions
React Router is a popular library for managing routing in React applications. However, a recent change has displayed a level of arbitrariness and laziness that may negatively impact developers who seek robust type checking.
Background
React Router is a popular library for managing routing in React applications. In our CIAM (Customer Identity and Access Management) solution - Logto - we are using React as well as React Router extensively in Admin Console and all crucial user flows, such as user sign-in and registration, for both OSS distributions and Cloud platform.
We have chosen React and React Router due to their strong communities and comprehensive TypeScript support, which is vital in modern JavaScript development.
React Router offers various hooks to access routing information, including the useLocation
hook. However, a recent change in the library's 6.9.0 release altered the type handling in the useLocation
hook, opting for the permissive any
type instead of the safer unknown
type. This decision has raised concerns among developers who prioritize type safety and rely on TypeScript's powerful type inference capabilities. By neglecting to provide accurate type annotations, React Router has displayed a level of arbitrariness and laziness that may negatively impact developers who seek robust type checking.
The importance of type safety in TypeScript
TypeScript is highly valued by developers for its ability to catch potential bugs at compile-time through static type checking. By providing a strong type system, TypeScript allows developers to express complex data structures, enforce constraints, and build more reliable and maintainable code. However, to fully harness the benefits of TypeScript, it is essential for libraries and frameworks to embrace type safety and ensure accurate type annotations at all times.
React Router's use of 'any' in the useLocation hook
The useLocation
hook in React Router is a critical tool for accessing the current URL's location object, which includes properties such as pathname
, search
, hash
, and state
. The problematic change introduced in version 6.9.0 revolves around the state
property, which allows the passing of arbitrary objects when navigating to another route. Previously, the useLocation
hook returned the state
property as the safer unknown
type. However, the recent update replaced it with the permissive any
type, removing all type information and opening the door to potential issues.
The pitfalls of 'any'
The use of the any
type in TypeScript poses significant pitfalls. By allowing any value to be assigned, it bypasses static type checking and undermines the benefits offered by TypeScript. Although it can provide a quick fix when the type is unknown, it should be used cautiously.
There are specific situations where the use of the any
type can be considered. For example, when migrating a JavaScript project to TypeScript, it can be utilized to handle variables that have not yet been migrated to their respective types. Additionally, in rare cases where third-party libraries utilize the any
type in their code, it may serve as a temporary workaround until a more appropriate solution can be implemented.
Overusing the any
type can introduce bugs, reduce code reliability, and hinder the advantages of static typing. Hence, any
is strongly not recommended in all lint rules.
Alternatives to 'any'
When you encounter a situation where the type of a variable is uncertain, use the unknown
type is the recommended approach, just like what the name suggests - unknown. Unlike the more lenient any
type, unknown
maintains a similar behavior but imposes stricter type checking.
One of the significant distinctions between any
and unknown
becomes apparent when attempting to assign them to other variables. If a variable belongs to the any
type, it can be assigned to another variable without encountering errors. However, trying to assign an unknown
type variable to a variable with a specific type will result in an error.
Furthermore, you can’t directly access properties from an unknown
type variable, until it has been narrowed down to a specific type through appropriate type checking. Learn more
Here’s an example of a simple type checking function that narrows down an unknown type to a known type, ensuring the safety of accessing the required properties from the object.
You can also utilize third party libraries such as zod or superstruct to write complex type guards and checker functions. (Learn more about type narrowing).
The need for 'unknown' in useLocation
As we have already mentioned above, when accessing and using the state
object from the useLocation
hook, the following lint errors arises and further causes build failures in our codebase.
If you are the developers who are facing the challenges after upgrading the library, what would you do? Disabling these lint errors with // eslint-disable-next-line
would be a quick workaround, but it undermines the very essence of TypeScript’s spirit, which revolves around leveraging static typing to detect potential errors early in the development process.
Consider the scenario where you pass an object through state
when navigating from page A to page B. If you disable the lint errors related to the any
state issue, TypeScript loses its ability to warn you about potential bugs. If you modify the object in page A but forget to update the corresponding code in page B, TypeScript won't catch the bug.
Utilizing the unknown
type, on the other hand, would force developers to apply appropriate type guards to narrow it down to a specific data type, which ensures the data type safety in both page A and page B, leading to more robust code and fewer runtime errors.
We tried to contact the developers of React Router, but their response was more or less like a “Nah, we are OK with any
, who bothers to use unknown
anyway.”
Well… As faithful TypeScript practitioners as we are, now what?
Overcoming the impact: Module augmentation through custom declaration files (d.ts)
Despite React Router’s reluctance to address this issue, developers can still mitigate the impact by leveraging custom declaration files (d.ts) in their own projects, the so called module augmentation. We had several attempts and then came up with a simplest solution. Here is an example of how to extend and augment the built-in type definitions of React Router with minimal effort:
In the above declaration, we import everything from the react-router-dom
package in the first place, and only extend the useLocation
hook by tailoring its return type. This ensures the useLocation
hook returning the correct state
type, leaving everything else intact in the React Router.
Conclusion
React Router's decision to switch from the safer unknown
type to the permissive any
type in the useLocation
hook is an arbitrariness and lazy approach that neglects the importance of type safety and TypeScript's benefits. This change creates challenge for developers who value type inference and strive for statically typed code.
However, developers can still overcome the impact and restore type safety by extending and overwriting the library's type definitions, through the use of a custom declaration file in their projects.
In the broader context, it is crucial for library maintainers to ensure type safety and provide accurate type definitions. This commitment ensures that all developers can fully leverage TypeScript's potential, resulting in a codebase that is more robust, maintainable, and less prone to errors. As the maintainers of Logto, we deeply value type safety and are committed to enhancing the developer experience by ensuring accurate type definitions and consistently delivering high-quality code.
If you haven’t heard about Logto, try it today for free!