Advanced ts: playing with generics in a React.Context
Tags: typescript, react, webdev
Published: 2023-03-31 12:11:44

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](https://react.dev/reference/react/createContext). Don't want to dig deep into its internals. We're ok checking just **how** we use it:

```tsx
import { XRouter, XRoute } from '@serious-company.react';
...

<XRouter>
  <XRoute path='/about' component={About} />
  <XRoute path='/checkout' component={Checkout} />
</XRouter>
```

&

```tsx
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:

```tsx
type AvailableRoutes = '/about' | '/checkout';
...

// ❌ tsc
const onClick = () => navigate('/ceckout');     

// ✅
const onClick = () => navigate('/checkout');

...

<XRouter>
  // ❌ tsc
  <XRoute path='/abbot' component={About} />
  // ✅      
  <XRoute path='/about' component={About} />
</XRouter>
```

👉 The answer is yep that's possible ... but also **No**

***

## [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)

Got down to work replacing each declaration of a _path_, from `string` to `T` where `<T extends string>`:

```diff
-type XRouterContextType {
-  navigate(path: string) => void;
-  history: string[];
+type XRouterContextType<T extends string> {
+  navigate(path: T) => void;
+  history: T[];
}
```

```diff
-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) => {
... 
```

```diff
-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](https://react.dev/reference/react/createContext#provider) (`<XRouter>` in these examples) and a way to consume that Context - probably - in a form of [`useContext()`](https://react.dev/reference/react/useContext) (`useXRouter()` in these examples)

Thing is, you need to **create** this context object:

```tsx
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: 

```tsx
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.

```tsx
// ❌
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:

```tsx
import { createXRouter } from '@serious-company.react';

type AvailableRoutes = '/about' | '/checkout';

const { XRouter, XRoute, useXRouter } = createXRouter<AvailableRoutes>();
...

const { navigate } = useXRouter();

// ❌ tsc
const onClick = () => navigate('/ceckout');       

// ✅
const onClick = () => navigate('/checkout');      
...

<XRouter>
  // ❌ tsc
  <XRoute path='/abbot' component={About} />      
  // ✅
  <XRoute path='/about' component={About} />      
</XRouter>
...
```

But this would mean changing the API of the package

```tsx
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 💛.