Skip to content

Instantly share code, notes, and snippets.

@micimize
Created December 30, 2017 19:06
Show Gist options
  • Select an option

  • Save micimize/be0105d4d1b757f15f30763ad00caa72 to your computer and use it in GitHub Desktop.

Select an option

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

micimize commented Dec 30, 2017

Copy link
Copy Markdown
Author

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