Redux & Typescript typed Actions with less keystrokes
a.k.a how to leverage discriminant unions and not get controlled by type system
I’ll be honest with you, I don’t write JavaScript without types, ever, so yeah I’m heavy Typescript user and time to time I play with Flow to see how the project evolves ( IMHO still the best option for typed JS is Typescript as of today, because various reasons…, but Flow is really on par nowadays and getting better everyday, so I guess it’s up to you which type checker are you gonna use for particular project).
So back to the topic shall we? I also use and love Redux for state management ( if it makes sense of course ! ) and having 100% type safe reducers, actions and data store is a must.
While I really like Typescript, I have to say it out laud: It’s super verbose to use it with Redux for strongly typed Actions. On the other hand, Flow is really rockin’ in this area because it has ability to alias & re-use the inferred type of a function’s return value (thanks to generics and existential type). If you wanna know more about Flows approach you should definitely check this superb article by Shane Osbourne, which partially made me write this post in first place and also to share with you how I approach the same problem within Typescript + Redux
In short, you can do following with Flow ( credits to Shane Osbourne ):
From:
const SET_NAME = 'SET_NAME';
const SET_AGE = 'SET_AGE';
type SetName = {type: typeof SET_NAME, payload: string}
type SetAge = {type: typeof SET_AGE, payload: number}
const setName = (name: string): SetName => {
return {type: SET_NAME, payload: name}
}const setAge = (age: number): SetAge => {
return {type: SET_AGE, payload: age}
}
export type Actions = SetName | SetAge;
To:
// Helper to extract inferred return type of a function
type _ExtractReturn<B, F: (...args: any[]) => B> = B;
type ExtractReturn<F> = _ExtractReturn<*, F>;// only need to provide types for arguments in action-creators
// return type will be inferred
function setAge(age: number) {
return { type: AGE, payload: age }
}function setName(name: number) {
return { type: NAME, payload: name }
}
// Create a union type containing all the return types of
// of your chosen action-creators.
// The result can be used as a tagged union
// that allows Flow to narrow the payload type
// based on 'type' property
type Actions =
ExtractReturn<typeof setAge> |
ExtractReturn<typeof setName>
Which I literally fall in love, because you don’t have to create artificially types for action creators and then type it again within implementation, which is concise and the relationship is: implementation → type annotation not in the opposite way!
So how can we achieve the same/similar approach with Typescript without creating too much repetition/type noise ?
We have 2 options as of today ( September 2017, TS 2.5 ):
1. Extract return type of a function:
Typescript has typeof operator as well, so you may think that we can do something like
function setAge(age: number) {
return { type: AGE, payload: age }
}
type SetAgeAction = typeof setAge
Well, you can, but this will get you signature of the function instead of return type of that function! huh!
Maybe just try this: type SetAgeAction = typeof setAge()
? Nope…
This is not supported by Typescript yet, but there is already a PR in play
So if this will ship to TS, you’ll be able to do something like:
function setAge(age: number) {
return { type: AGE, payload: age }
}function setName(name: string) {
return { type: NAME, payload: name }
}
// Create a union type containing all the return types of
// of your chosen action-creators. The result can be used as a discriminant union that allows flow to narrow the payload type based on 'type' property
type Actions =
typeof setAge(0) |
typeof setName('')
But we are not there yet! Don’t you worry, we can apply some “hacks” to achieve something similar.
Check this out:
// our Action creators
function setAge(age: number) {
return { type: SET_AGE as typeof SET_AGE, payload: age }
}function setName(name: number) {
return { type: SET_NAME as typeof SET_NAME, payload: name }
}// Our return type runtime function -> our nasty "hack"
const getReturnType = <R>(f: (...args: any[]) => R): R => null!const SetNameType = getReturnType(setName)
const SetAgeType = getReturnType(setAge)type Actions = typeof SetNameType | typeof SetAgeType
Bleh! WHAT is this abomination dude?! are you crazy?
Well I warned you, that it will be a hack. Ok,What’s going on here ?
Well because Typescript is unable to infer proper function return types via generics we need to create runtime calls and temporary variables to get proper return proper type.
- return { type: SET_AGE as typeof SET_AGE, payload: age } — this explicit casting to type literal is needed, so type system has 100% guarantee, that this literal type will be immutable
If you have concerns about code bloat et all, let me assure you that it will be removed by your smart minifier like Uglify, because those variables are not used anywhere in real runtime code, so not that bad…
2. Using Classes and leveraging structural nature of Typescript
So Typescript is structural type system, and yes class is just a typed structure by shape. Which makes us leverage following pattern:
const SET_NAME = 'SET_NAME'
const SET_AGE = 'SET_AGE'class SetName {
readonly type = SET_NAME
constructor(public payload: string) {}
}class SetAge {
readonly type = SET_AGE
constructor(public payload: number) {}
}type Actions = SetName | SetAge
Boom! nice and short! isn’t it ? This pattern wasn’t invented by me haha, kudos goes to folks from angular’s ngrx project ( #highFive ).
I see you concerns here, you may be thinking right now:
What? this isn’t even javascript dude, what the hell!?
Well it is javascript, just with some Typescript sugar, so we don’t have to write that much ( which is what we want, all in all the whole post is about less typing and implementation → type annotation relationship )
- readonly -> we enforce immutability and we are saying to TS that this will not change so it can infer the value of the type literal ( so it won’t be of type string, rather value of SET_NAME which is string literal ‘SET_NAME’ )
- constructor(public payload: string) {} -> this is just TS sugar for writing following by hand :
constructor(payload){
this.payload = payload
}
Last but not least, when creating your actions, you’ll have to new up, because we are using classes. Good approach here is to hide this implementation detail within your mapDispatchToProps functions so you don’t get that new
bloat within your component code.
Oh and I almost forgot to mention one last thing :), you have to cast these actions to pure objects within custom middleware, otherwise Redux will throw an error ( it accepts only pure objects, with Object on prototype ). You can quickly write something like this:
import { MiddlewareFn } from 'redux'type Actions = ... // all action unions
type State = ... // all substores unionsconst actionToPlainObject: MiddlewareFn<State, Actions> = store => next => action => { if (isObjectLike(action)) {
return next({ ...action })
}
throw new Error(`action must be an object: ${debug(action)}`)
}function isObjectLike(val: any): val is {} {
return isPresent(val) && typeof val === 'object'
}
function isPresent(obj: any): obj is Present {
return obj !== undefined && obj !== null
}
Summary
Here is a comparison, side by side with all 3 approaches:
- Left pane uses traditional approach by creating Type aliases manually and then using them to annotate the return type of our action creators : Relation Types → implementation
- Center and right approach are effective ways how to mitigate former issue of terms of, how your app gets type annotations. You get the types from implementation so you don’t have to write things down twice, so your action creator is your type: Relation Implementation → types. This is very important so we are not controlled by Type System
- Clear win here is leveraging classes because it’s most concise and clear without any hack, of course until formerly mentioned feature will not land in TS
So that’s it! Hope you’ve learned something new today! Happy type checking and ‘till next reducer call folks!
Links
- Extract return type of a function (Typescript playground)
- Classes as Actions Creators (Typescript playground)