Redux & Typescript typed Actions with less keystrokes

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;
// 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>

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
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('')
Hacks you say?!
// 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
  • 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

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
  • 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
}
import { MiddlewareFn } from 'redux'type Actions = ... // all action unions
type State = ... // all substores unions
const 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:

Types -> implementation, Implemetation -> types with function return type hack, Implementation -> types with structural features of Typescript and classes
  • 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

Links

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Martin Hochel

Martin Hochel

Principal Engineer | Google Dev Expert/Microsoft MVP | @ngPartyCz founder | Speaker | Trainer. I 🛹, 🏄‍♂️, 🏂, wake, ⚾️& #oss #js #ts