The power of advanced typing in Typescript
Tags: javascript, typescript, typing
Published: 2020-10-23 03:09:24

When I started working on version 3 of S-Flux, I really wanted to facilitate the use of the Application Stores and the methods to interact with them.

To do this, I wanted to leverage the power of advanced Typescript typing.

### The inputs
Let's take a quick look at the inputs.

```ts
export default Flux.registerStore({
  id: "Counter",
  async init(): CounterState { 
    return { counter: 0 };
  },
  actions : _actions,
  events  : _events
});
```

We define a unique Id, an init function to initialize the state, some actions and some events.

An action is an async method whose first parameter is the Payload.
```ts
// This parameter is not a real parameter :-)
async setCounter(this: TThis, payload: { counter: number }) { 
  /* ... */
  return _events.ForceCounter; // Return specific event 
}
```

An event is a key/value pair defining an event that can be returned by a store with a specific subscription.

### The target output
Now we need to see what we want as an output.

```ts
{
  getState() : CounterState ;
  id         : string       ;
  events: {
    All          : string ;
    ForceCounter : string ;
  },
  subscribeTo: {
    All          : ((state: CounterState) => void) => { off: () => void };
    ForceCounter : ((state: CounterState) => void) => { off: () => void };
  },
  actions: {
    increase   : () => void                             ;
    decrease   : () => void                             ;
    setCounter : (payload: { counter: number }) => void ;
  }
}
```

In this example, the store wrapper will expose new methods based on the input types.

Events represents the same events as in the input, but for each event a corresponding method is generated in the `subscribeTo` object to register to the event when emitted by the store, plus a 'All' handler to catch all events.

Same for the `actions` object. For each action handler that has been registered, the counterpart method should be exposed to call the handler with the correct payload.

With this approach we will avoid the need to create manually some action helpers for each possible action.

### The problem
If we create the wrapper manually, everything will be typed correctly, but the point is that the code will be created dynamically in the registerStore function.

So the return type will be `any` for the `State`, the `actions` and `events` objects. That's why we need to use the *advance typing system* to provide a fully typed object with all methods even for code that will be dynamically produced. 

It is exactly for this kind of feature that the use of typescript rather than javascript seems obvious to me.

### Preparing the types
Let's see the input type as it is defined in the registerStore method signature : 

```ts
export type TStoreDefinition<S extends (...args: any[]) => any, T extends {[key: string]: (...args: any[]) => any}, U> = {
  id              ?: string                      ;
  localActions    ?: boolean                     ;
  actions          : T                           ;
  events          ?: U                           ;
  mappedActions   ?: { [key: string] : string; } ;
  init             : S                           ;
  dispatchHandler ?:(this: TBaseStore<ReturnType<S>>, payload: any, For? : TAwaitFor) => Promise<void | null | string | string[]> ;
  nextState       ?: (newState: Partial<ReturnType<S>>, mergeToPreviousState?: boolean) => void                                   ;
}
```

We need to infer types from `actions`, `events`, and `init`.
* __actions__ contains keys that we want to extract
* __events__ contains keys that we want to extract
* __init__ contains a return type equal to the store State, so we need to extract it too.

To allow typescript to infer those types and to work with them, we need to dedicate a type for `actions` `events` and `init`, and to ensure that the input is correct, we add some constraint when declaring them.

### Generating the types dynamically
Let's see how to expose the output methods of the `actions` object from the input `actions` field :-)

#### Extracting the field type.
We can use the PropType type to get a specific field type.
```ts
type TActions = PropType<typeof def, "actions">;
```

PropType type is itself declared as follow : 
```ts
export type PropType<TObj, TProp extends keyof TObj> = TObj[TProp];
```
When declaring 'TProp extends keyof TObj', we will get all real keys of TObj, that's how we will have all typing for keys we don't know yet.

Now 'TActions' is of type T with the constraint defined in the signature

```ts
T extends {[key: string]: (...args: any[]) => any}
```

TActions is infered, so it is not only a hashmap with a key of type string and a value as a method, it contains the correct keys declared in the input. **That's the important part.** Each key is fully declared and can be used.

#### Creating the target type
We need to create the target type that will expose the same keys but with diffrent values.

```ts
export type TActionExtention<T, U> = {
  [P in keyof T]: 
  ((this: TBaseStore<U>, payload: any) => Promise<void | null | string | string[]>) | 
  ((this: TBaseStore<U>, payload: any, For: TAwaitFor) => Promise<void | null | string | string[]>)
};
```

`[P in keyof T]` ensures that every named key in T will have some constraints defined just after.

So in our case, every value of each key will be of one of the 2 types below.

* `((this: TBaseStore<U>, payload: any) => Promise<void | null | string | string[]>)`
* `((this: TBaseStore<U>, payload: any, For: TAwaitFor) => Promise<void | null | string | string[]>)`

We need to iterate on input keys to output the new defined types

```ts
const _actions = {} as {
  [P in keyof TActions] :  /* ... */
};
```

`_action` has the same keys as `TActions`, and TAction is a type infered from the `actions` field in the input.

#### Conditional typing

We need to create a method type that will have as first parameter the correct payload. So we have to extract the payload type from the first parameter of the input method.

We can extract it like that :
```ts
PropType<Parameters<TActions[P]> , 0>
```

`Parameter` will extract the parameters from `TActions[P]` as a hashmap where the key is the parameter index, and the value the parameter type itself.

`PropType<..., 0>` will extract the property type for the field named 0.

So the definition can now be 

```ts
const _actions = {} as {
    [P in keyof TActions] :  (payload: PropType<Parameters<TActions[P]> , 0>) => void
  };
```

Yes !!! but wait a minute... The input method can have a payload.... or not, so this parameter can be undefined.

This is when the `Wouaou effect occurs`. You can define types for something you don't know... and you can even test the type to add a condition... It is just amazing.

So the final type will look like this

```ts
const _actions = {} as {
    [P in keyof TActions] :  PropType<Parameters<TActions[P]> , 0> extends undefined ?
    (() => void) 
    :
    ((payload: PropType<Parameters<TActions[P]> , 0>) => void) 
  };
```

And now we have a deal ! The result is a fully type object defining all methods we need with strongly types signatures to avoid syntax / typing errors at runtime.

That is why I love typescript so much :-)

And to finish, we want to expose a getState method that will return a value equals to the store state. We never defined directly a store state, but we have defined an init function whose return type is the one we need.

So to extract our store state we will do the following
```ts
type TState = ReturnType<PropType<typeof def, "init">> ;
```

In 3 steps, we will get the def type, then extract the init field, which is a method, then its return type.

### Conclusion
I hope this post will help you to see how powerfull Typescript's typing is.

If you want to know more you can check the Typescript documentation about advanced types : [Link here](https://www.typescriptlang.org/docs/handbook/advanced-types.html)

To see the source code of the example you can check [S-Flux here ](https://nomoredeps.github.io/shadowjs)

Enjoy.