This content originally appeared on DEV Community and was authored by Manuel Artero Anguita 🟨
This is a story of failure.
I wasn't going to write this down... then I thought, well not all the stories need a happy ending.
To the point.
We have a custom React Context. Don't want to dig deep into its internals. We're ok checking just how we use it:
import { XRouter, XRoute } from '@serious-company.react';
...
<XRouter>
<XRoute path='/about' component={About} />
<XRoute path='/checkout' component={Checkout} />
</XRouter>
&
import { useXRouter } from '@serious-company.react';
...
const { navigate } = useXRouter();
const onClick = () => navigate('/checkout');
...🤔 I wonder...
Is it possible to set the exact set of routes?
so both <XRoute />
and navigate()
would narrow the type of the path from string to one of my routes?
I was looking for this:
type AvailableRoutes = '/about' | '/checkout';
...
const onClick = () => navigate('/ceckout'); // ❌ tsc
const onClick = () => navigate('/checkout'); // ✅
...
<XRouter>
<XRoute path='/abbot' component={About} /> // ❌ tsc
<XRoute path='/about' component={About} /> // ✅
</XRouter>
👉 The answer is yep that's possible ... but also No
Generics
Got down to work replacing each declaration of a path, from string
to T
where <T extends string>
:
-type XRouterContextType {
- navigate(path: string) => void;
- history: string[];
+type XRouterContextType<T extends string> {
+ navigate(path: T) => void;
+ history: T[];
}
-function XRouter({ children }: XRouterProps) {
- const [history, setHistory] = useState(['/'])
-
- const navigate: (path: string) => {
+function XRouter<T extends string>({ children }): XRouterProps) {
+ const [history, setHistory] = useState<T[]>(['/')
+
+ const navigate: (path: T) => {
...
-type XRouteProps {
- path: string;
+type XRouteProps<T> {
+ path: T;
component: ComponentType;
}
-function XRoute({ path, component }: XRouteProps) {
+function XRoute<T extends string>({ path, component }): XRouteProps<T> {
...
And everything was pure and flawless, just like JavaScript...
...till the context object
Creating a React context, we'll export a Context Provider (<XRouter>
in these examples) and a way to consume that Context - probably - in a form of useContext()
(useXRouter()
in these examples)
Thing is, you need to create this context object:
const context = createContext<XRouterContextType>({ ... })
This object has a well defined type. You just can't create an object but keep it with a futuristic Generic T.
🖋️ A way of understanding Generics in Typescript might be:
"We just don't know T yet.
T will be set by the caller of the function.
They will know.
For now, T is unset"
So, creating an object:
const context = createContext<XRouterContextType>({ ... })
You are the caller. You need to set the type. You can't create an object with a "we'll see" type.
const context = createContext<XRouterContextType<T>>({ ... }) // ❌
So, is this an impossible problem?
Well, we may implement a work around... exporting a closure function - call it factory, call it Class, call it High Order function, call it whatever you feel - which, when called: creates the context object and returns the Provider component and the hook.
I got a branch with this working:
import { createXRouter } from '@serious-company.react';
type AvailableRoutes = '/about' | '/checkout';
const { XRouter, XRoute, useXRouter } = createXRouter<AvailableRoutes>();
...
const { navigate } = useXRouter();
const onClick = () => navigate('/ceckout'); // ❌ tsc
const onClick = () => navigate('/checkout'); // ✅
...
<XRouter>
<XRoute path='/abbot' component={About} /> // ❌ tsc
<XRoute path='/about' component={About} /> // ✅
</XRouter>
...
But this would mean changing the API of the package
import { XRouter } from '@serious-company.react';
=>
import { createXRouter } from '@serious-company.react';
const { XRouter } = createXRouter<'/a'|'/b'>();
And we finally discarded this work.
As I said, this is a story of failure. At least I did have a good time 🤘.
Thanks for reading 💛.
This content originally appeared on DEV Community and was authored by Manuel Artero Anguita 🟨
Manuel Artero Anguita 🟨 | Sciencx (2023-03-31T12:11:44+00:00) Advanced ts: playing with generics in a React.Context. Retrieved from https://www.scien.cx/2023/03/31/advanced-ts-playing-with-generics-in-a-react-context/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.