Skip to content

Instantly share code, notes, and snippets.

@micimize
Created December 30, 2017 19:06
Show Gist options
  • Save micimize/be0105d4d1b757f15f30763ad00caa72 to your computer and use it in GitHub Desktop.
Save micimize/be0105d4d1b757f15f30763ad00caa72 to your computer and use it in GitHub Desktop.
thoughts on how we might accomplish the power of reasonml with typescript
type NotFunction = object | string | boolean | number
type Block<Return> = () => Return
function isBlock<Return>(r: Return | Block<Return>): r is Block<Return> {
return typeof r === "function"
}
type Cases<Tuple, Return extends NotFunction = object> = Array<[
Tuple,
Return | Block<Return>
]>
function equals(a, b){
return JSON.stringify(a) === JSON.stringify(b)
}
/*
* Build a switch that keys off of tuples with a required default case
* type Tuple = ['a' | 'b' | 'c', '1' | '2' | '3' ]
* let cases = [
* [ ['a', '1'], () => 'a1' ],
* [ ['b', '2'], 'b2' ],
* ]
* let defaultCase = 'default'
* Switch<Tuple, 'a1' | 'b2' | 'default'>(['a', '1']) //=> 'a1'
* // TODO: cache stringified tuples
*/
function Switch<Tuple extends Array<string>, Return extends any = any>(
cases: Cases<Tuple, Return>,
defaultCase: Return | Block<Return>
) {
return (value: Tuple | string): Return => {
if(typeof(value) === 'string'){
return isBlock(defaultCase) ? defaultCase() : defaultCase
}
for (let [ tuple, result ] of cases){
if(equals(value, tuple)){
return isBlock(result) ? result() : result
}
}
return isBlock(defaultCase) ? defaultCase() : defaultCase
}
}
/*
* Helper wrappers around the above switch that creates switch factory for the given keys
* DEFAULT is always required
* let dictSwitch = Switch.Dict({ a: ['a', '1'], b: ['b', 2] })
* dictSwitch({ a: 'a1', b: 'b2', DEFAULT: 'default' })(['b', 2]) //=> 'b2'
*/
namespace Switch {
export function Dict<Key extends string, Tuple extends Array<string>>(mapping: Record<Key, Tuple>){
type Default<Return> = { DEFAULT: Return | Block<Return> }
type CaseDict<Return> = Record<Key, Return | Block<Return>>
// D is the dictionary of cases
function DictSwitch<Return extends any = any, D = CaseDict<Return>>(caseDict: D & Default<Return>){
let defaultCase = caseDict.DEFAULT
delete caseDict.DEFAULT
let cases: Cases<Tuple, Return> = Object.entries(caseDict).map(
([ key, result ]) => [ mapping[key], result ] as [Tuple, Return | Block<Return>])
return Switch<Tuple, Return>(cases, defaultCase)
}
type DictSwitch = {
// The root switch is exhaustive
<Return extends any = any>(caseDict: CaseDict<Return> & Default<Return>),
// All we need for a partial alternative is type casting
partial<Return extends any = any>(caseDict: Partial<CaseDict<Return>> & Default<Return>)
}
let S = <DictSwitch>DictSwitch
S.partial = <DictSwitch['partial']>DictSwitch
return S
}
}
@micimize
Copy link
Author

micimize commented Dec 30, 2017

the goal here is to end up with ReasonML switch statements and pattern matching. The tuple stuff here might be unnecessary - really the potentially compelling use case is predefined, type safe DSLs.
An example usage (more fleshed out in redux-routines, although still lacking):

enum RoutineAction {
  Trigger = 'TRIGGER',
  Request = 'REQUEST',
  Success = 'SUCCESS',
  Failure = 'FAILURE',
}

type RoutineActions<Prefix extends string> = {
  TRIGGER: [ Prefix, RoutineAction.Trigger ],
  REQUEST: [ Prefix, RoutineAction.Request ],
  SUCCESS: [ Prefix, RoutineAction.Success ],
  FAILURE: [ Prefix, RoutineAction.Failure ],
}

function routineActions<Prefix extends string>(prefix: Prefix): RoutineActions<Prefix> {
  return {
    TRIGGER: [ prefix, RoutineAction.Trigger ],
    REQUEST: [ prefix, RoutineAction.Request ],
    SUCCESS: [ prefix, RoutineAction.Success ],
    FAILURE: [ prefix, RoutineAction.Failure ],
  }
}

// leaving out creator logic
function createRoutine<Prefix extends string>(prefix: Prefix){
  let actions = routineActions(prefix)
  return {
    actions,
    switch: Switch.Dict(actions),
  }
}

// usage
let fetchRoutine = createRoutine<'FETCH'>('FETCH')

fetchRoutine.switch.partial<{ state?: 'LOADING' | 'COMPLETED' | 'FAILED' }>({
  TRIGGER: { state: 'LOADING' },
  DEFAULT: {}
})

// will throw a type error, because the provided switch is not exhaustive
fetchRoutine.switch<{ state?: 'LOADING' | 'COMPLETED' | 'FAILED' }>({
  TRIGGER: { state: 'LOADING' },
  DEFAULT: {}
})

// default probably shouldn't be required
fetchRoutine.switch<{ state?: 'LOADING' | 'SUCCESS' | 'FAILURE', data?: any }>({
  TRIGGER: {},
  REQUEST: { state: 'LOADING' },
  SUCCESS: { state: 'SUCCESS', data: 'whatever' },
  FAILURE: { state: 'FAILURE', data: 'oh no' },
  DEFAULT: {}
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment