Being a frequent Typescript user I’m fully onboard with the benefits of Type systems. Especially in the arena of UI development with React & React-Native, I don’t think I could ever imagine going back to a time where your props & state updates are not verified by the compiler as you type…
Typescript is great, no complaints from me, but one thing that really caught my eye recently about Flow was this ability to alias & re-use the inferred type of a function’s return value.
That means, given a function that has it’s arguments annotated, and performs no side-effects, Flow will be able to work out the ‘shape’ or the ‘type’ of a return value, and allow you to use that elsewhere.
I know it’s mouthful, but bear with me. Here’s a snippet I found whilst digging through about 2000 github issue threads. I’ve seen others, but this seems to work well, so…
type _ExtractReturn<B, F: (...args: any[]) => B> = B;export type ExtractReturn<F> = _ExtractReturn<*, F>;
We’re exporting a generic type called **ExtractReturn**
that accepts another type (in this case, the ‘typeof’ a function), and extracts the inferred return type — to use it with the function above, you’d use typeof setName
like this:
import {setName} from './actions';import {ExtractReturn} from './types';
type ReturnValue = ExtractReturn<typeof setName>
Don’t worry if you don’t understand exactly what’s going on here, just copy/paste the snippet and move onto the next bit — I’m being deliberately quite hand-wavey here as I want to focus on the point of this blog (and the fact I’m no type-system expert myself)! But anyway, onto the good stuff!
The most common approach I’ve seen to providing type-safety in a Redux Application is to have the shape of each action defined separately from your actual function implementations. So you’d typically define all of the action objects your application is ‘allowed’ to use, and then you’d re-use those types in action-creators and reducers. It normally looks something like this:
Before:
export const SET_NAME = 'SET_NAME';export const SET_AGE = 'SET_AGE';
export type SetName = {type: 'SET_NAME', payload: string}export type SetAge = {type: 'SET_AGE', payload: number}
const setName = (name: string): SetName => {return {type: SET_NAME, payload: name}}
const setAge = (age: number): SetAge => {return {type: SET_NAME, payload: age}}
export type Actions = SetName | SetAge;
Of course this example is tiny with just 2 actions creators and I’ve condensed it down onto 1 imaginary file for the sake of space, but it’s enough to explain the concepts here.
Defining each action ‘shape’ as it’s own type as seen on lines 3 and 4, means that you can be very specific about an object/action that a function should return. Given that you’ve had to declare them all up front like this, it gives very strong guarantees that you’re not going to be dispatching unexpected object shapes into your Redux store. Awesome.
Crucially though, this technique also allows you to create what’s known as a ‘tagged union’ as seen on the last line. It’s known as a ‘discriminated union’ in Typescript land, and is basically a way of a narrowing any type-checking based on the value of a particular field — in our case it’s the ‘type’ field from the object.
Flow calls it a ‘tagged union’, whilst in TS land it’s known as a ‘discriminated union’ — very powerful.
So with this technique of defining all actions as separate types, we can both enforce the return-types of action-creators whilst also narrowing our type-checking in reducers based on which action was fired. All sounds great, right?
It is great actually — I’ve used this technique before without any real issues, but I still find it interesting to experiment with these type systems in an attempt to extract as much value from them with as little typing as possible.
So looking back at the previous code, my personal opinion is that having to come up with new names for each and every Action, duplicating the action name, and having to annotate the return value of every action-creator are all things that can be avoided.
So with that in mind, if we just make use of the ExtractReturn
helper from before, the previous example could be be reduced to the following:
After:
export const SET_NAME = 'SET_NAME';export const SET_AGE = 'SET_AGE';
const setName = (name: string) => {return {type: SET_NAME, payload: name}}
const setAge = (age: number) => {return {type: SET_AGE, payload: age}}
export type Actions =ExtractReturn<typeof setName> |ExtractReturn<typeof setAge>
The fact that we’ve reduced the duplication of the action names themselves is great, but we’ve also removed the need to come up with separate names for our actions — we all know just how hard naming things are, right?!
The exported type Actions
on the last line will also still generate the ‘tagged union’ as described previously, meaning we’re not losing out on any of the (awesome) narrowed type-checking that we were getting before — yay!
Perhaps most interestingly though, is that we’ve now inverted the source-of-truth to be our actual code, rather than the separate types. This means any changes to the return values in any action-creators are automatically propagated throughout the system — there’s no need to update any separate types!
More type-safety per keystroke, and less duplication!
This is a trade-off that some people will not be comfortable with, and I would totally understand that — I can see the benefit of having all action object types explicitly defined separately from the code (as I said, I’ve used this approach before without issue).
But, having worked on large typed projects (both in Typescript & Flow) I’ve ended up with a few opinions about this type of stuff:
When I use phrases such as ‘less typing’ and ‘fewer key strokes’ I’m NOT talking about creating ‘shorter’ code in terms of actual characters — I’m talking directly about having less *things* to create, import, export, name & maintain. This blog post has shown a way of producing very similar type-safety with dramatically less code (when you consider a real code-base).
Hope you enjoyed this experiment!
Like this? If you did then perhaps you’d enjoy some of my lessons on https://egghead.io/instructors/shane-osbourne— many are free and I cover Vanilla JS, Typescript (soon Flow?), RxJS and more.