Skip to content

Instantly share code, notes, and snippets.

@fvilante
Last active October 30, 2020 09:55
Show Gist options
  • Save fvilante/d2a342d86996abaa44fba4aa22e94603 to your computer and use it in GitHub Desktop.
Save fvilante/d2a342d86996abaa44fba4aa22e94603 to your computer and use it in GitHub Desktop.
ReasonML inspired pattern match for Typescript
// ========================================================================================
// Utilities types
// ========================================================================================
type TU11 = '(T extends U) and (U extends T)'
type TU10 = '(T extends U) and (U NOT extends T)'
type TU01 = '(T NOT extends U) and (U extends T)'
type TU00 = '(T NOT extends U) and (U NOT extends T)'
type Ext<T,U,A,B> = T extends U ? A : B // In type system jargon we can say that "T is assignable to U".
type ExtendsCheck_<T,U> = T extends U ? (U extends T ? TU11 : TU10) : (U extends T ? TU01 : TU00)
type ExtendsCheck<T,U> = Ext<T,U,Ext<U,T,TU11,TU10>,Ext<U,T,TU01, TU00>>
type ExtendsCheck__<T,U> = ExtendsCheck<ExtendsCheck<T,U>,ExtendsCheck<U,T>>
type IsEqual<A,B,X=true,Y=false> = ExtendsCheck<A,B> extends TU11 ? X : Y
type IsNotEqual<A,B,X=true,Y=false> = IsEqual<A,B,Y,X>
type UnionFilterByExclusion<T, K> = T extends K ? never : T
type UnionFilterByInclusion<T, K> = T extends K ? T : never // todo: when possible discover why Ext<T,K,T,never> do not works
type TMap_<T> = {kind: T }
type ToBeEqual<T,U> = ExtendsCheck<T,U> extends TU11 ? true : false
type AssertsEqual<Expected, Actual> = ToBeEqual<Expected,Actual>
type TestAll<T extends true[]> = T
// ========================================================================================
// TupleInference
// ========================================================================================
type FilterTuple<T extends unknown[]> = T['length'] extends number ? T['length'] : T['length']
type U70 = [1,2,3]| [10,20,30]
type U71 = number[] | string[]
type U72 = U70 | U71
type S70 = FilterTuple<U70>
type S71 = FilterTuple<U71>
type T70 = AssertsEqual<S70,U70>
const tail = <T extends unknown[]>(arr: readonly [unknown, ...T]) => {
const [_ignored, ...rest] = arr;
return rest;
}
type IsLessThenOrEqual<N extends number> =
N extends 0 ? 0 :
N extends 1 ? 0 | 1:
N extends 2 ? 0 | 1 | 2:
N extends 3 ? 0 | 1 | 2 | 3:
N extends 4 ? 0 | 1 | 2 | 3 | 4:
number
type List<S,A extends unknown[]> =
S extends 0 ? [] :
S extends 1 ? [A[0]] :
S extends 2 ? [A[0], A[1]] :
S extends 3 ? [A[0], A[1], A[2]] :
S extends 4 ? [A[0], A[1], A[2], A[3]] : never
type TupleOf<T, N extends number> =
N extends N ? _TupleOf<T,N,[]> : never
type _TupleOf<T, N extends number, R extends unknown[]> =
R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;
type = InferTuple<[1,2,3,4,5]>
//type TT = TupleOf<number,5>
const take = <H extends unknown[], T extends unknown[]>(size: S, arr: readonly [unknown, ...H]) => {
const [_ignored, ...rest] = arr;
return rest;
}
const t050 = tail([1,2,3,4] as const)
// unit test
type U60 = number | [1,2,3,4] | string
type U61 = [1, 2, 3, 4]
type S60 = FilterTuple<U60>
type T60 = AssertsEqual<S60,U61>
//type S52 = MatchTuple<S50,[1,2]>
const matchList = <As extends unknown[], Bs extends unknown[]>(list: As, match: Bs): [Bs, ] => {}
// ========================================================================================
// Variant Lib
// ========================================================================================
type Variant<K extends string = string, A = unknown> = [
value: A,
ctor: K,
kind: "Variant",
]
type VariantConstructor<K extends string, A> = {
readonly kind: "VariantConstructor"
readonly ctor: K
readonly run: (data:A) => Variant<K,A>
} & ((data:A) => Variant<K,A>)
type VariantCtor<V extends Variant> = V[1]
type VariantValue<V extends Variant> = V[0]
declare const VariantConstructor: <V extends Variant>(k: VariantCtor<V>) => (data:VariantValue<V>) => Variant<VariantCtor<V>,VariantValue<V>>
// unit test
// NoBytes
type NoBytes = Variant<"NoBytes",undefined>
const NoBytes = VariantConstructor<NoBytes>('NoBytes')
// Byte(number)
type Byte = Variant<"Byte",number>
const Byte = VariantConstructor<Byte>("Byte")
// Bytes(number[])
type Bytes = Variant<"Bytes",number[]>
const Bytes = VariantConstructor<Bytes>("Bytes")
// type Data = NoBytes | Byte(number) | Bytes(number)
type Data = NoBytes | Byte | Bytes
const Data = VariantConstructor<Data>("Bytes")
declare const data: Data
// ========================================================================================
// Pattern Match lib
// ========================================================================================
// lib single
type Exhausted = "exhausted"
type Pending = "pending"
type Unused = "unused"
type E = Exhausted | Pending
type State<C extends E = E, T = unknown> =
C extends Exhausted ? { state: Exhausted, types: never } :
C extends Pending ? { state: Pending, types: T extends T ? { pending: T} : never}
: never
type Distribute<T> = T extends T ? T : never
type IfIsPendingDo<S extends State, Do> = Distribute<SwitchState<S> extends Pending ? Do : never>
type IfStateIs<S extends State,C extends E,Do> = S extends State<C,unknown> ? Do : never
type PendingTypes<S extends State> = S['types']['pending']
type SwitchState<S extends State> = S['state']
// unit test
type U20 = number | string | Data
type S19 = State<Pending, U20>
type S20 = PendingTypes<S19>
type S21 = SwitchState<S19>
type T20 = AssertsEqual<S21, Pending>
type T21 = AssertsEqual<S20, U20>
type T22 = TestAll<[T20,T21]>
type Remove<T,U> = T extends T ? Exclude<T,U> : never
type Next<S extends State,U extends PendingTypes<S>> =
IfIsPendingDo<S,
// yes it's pending, check if following is the last step
State<Pending,Remove<PendingTypes<S>,U>> extends State<Pending,never>
// yes, last step
? State<Exhausted>
// no, continue
: State<Pending,Remove<PendingTypes<S>,U>>
>
// unit test
type U00 = string | number | 1[] | Data
type S00 = State<Pending,U00>
type S01 = Next<S00,number>
type T01 = AssertsEqual<PendingTypes<S00>,U00>
type T02 = AssertsEqual<UnionFilterByExclusion<U00,number>,PendingTypes<S01>>
type T03 = AssertsEqual<[Pending, Pending], [SwitchState<S00>,SwitchState<S01>]>
type U01 = Bytes | Byte | 1[]
type S02 = Next<S01, U01>
type T04 = AssertsEqual<S02,State<Pending, UnionFilterByExclusion<PendingTypes<S01>,U01>>>
type U02 = string | Variant<"NoBytes", undefined>
type S03 = Next<S02, U02>
type T05 = AssertsEqual<S03,State<Exhausted, never>>
//type S04 = Next<S03, 2[]> //must be an error
type T06 = TestAll<[T01,T02,T03,T04,T05]>
type FilterVariant<T> = UnionFilterByInclusion<T,Variant>
type FilterNonVariant<T> = UnionFilterByExclusion<T,Variant>
type DefaultResultState<S extends State> = IfStateIs<S,Pending,State<Exhausted>>
type SelectVariant<T extends Variant, K extends string> =
T extends Variant<K, infer A> ? Variant<K,A> : never
type VariantSugar<K extends string = string> = { variant: K }
type Pattern<S extends State = State> = IfStateIs<S, Pending,
| FilterNonVariant<PendingTypes<S>>
| { variant: VariantCtor<FilterVariant<PendingTypes<S>>> } // VariantSugar
>
type Data_<S extends State = State, P extends Pattern<S> = Pattern<S>> =
P extends { variant: string } // // VariantSugar
? SelectVariant<FilterVariant<PendingTypes<S>>,P['variant']>
: P
type Step<S extends State, P extends Pattern<S>> = Data_<S,P>
type Match<S extends State = State,R = unknown> = {
with: <P extends Pattern<S>>(pattern: P, match: (data: Data_<S,P>) => R) => Match<Next<S,Step<S,P>>,R>
when: <P extends Pattern<S>>(pattern: P, whenCondition: (_: Data_<S,P>) => boolean, match: (data: Data_<S,P>) => R) => Match<S,R>
//WithList: <L extends PatternList<S>>(pattern: L) => undefined
default: (match: (data: PendingTypes<S>) => R) => Match<DefaultResultState<S>,R>
run: () => IfStateIs<S,Exhausted,R>
getState: () => S
}
declare const Match: <A,R>(data:A) => Match<State<Pending,A>,R>
declare const Switch: <R>() => <A>(data:A) => Match<State<Pending,A>,R>
// unit test
type U40 = PendingTypes<S02>
type U39 = 'Bytes'
type U41 = S02
type S40 = Pattern<U41>
type U42 = { variant: 'NoBytes'}
type S41 = Data_<U41, U42>
type S42 = Step<U41, U42>
type S43 = Next<U41,S42>
type S44 = Match<S43>
type T40 = AssertsEqual<S42,S41>
type T41 = AssertsEqual<S42,Variant<'NoBytes',undefined>>
type T42 = AssertsEqual<S43,State<Pending,string>>
type T43 = TestAll<[T40,T41,T42]>
type T44 = TestAll<[...T22, ...T06, ...T43]>
// unit test
type Q0 = 0
type Q1 = 1
type Q2 = 2
type Q = Q0 | Q1 | Q2
declare const q0: Q0
declare const q1: Q1
declare const q2: Q2
declare const q: Q
type X0 = `a`
type X1 = `b`
type X2 = `c`
type X = X0 | X1 | X2
declare const x0: X0
declare const x1: X1
declare const x2: X2
declare const x: X
type S11 = ReturnType<Match<S00,undefined>['getState']>
type T10 = AssertsEqual<S11, ["pending", U00]>
const log = (msg: string):undefined => { console.log(msg); return undefined}
// enviroment
const is8Bits = (byte: number):boolean => byte >=0 && byte <= 255
declare const toMatch: Q | Data
// type Q = 1 | 2 | 3
// type Data = Byte(number) | Bytes(number[]) | NoBytes // variants
// Test
const T11 = Switch<undefined>()(toMatch)
.with({ variant: 'NoBytes'}, _ => undefined)
.with(0, data => undefined)
.with(1, data => undefined)
.with(2, data => undefined)
.when({variant: 'Byte'}, ([byte]) => is8Bits(byte), data => undefined)
.when({variant: 'Bytes'}, ([bytes]) => bytes.every(is8Bits), data => undefined )
.default( data =>
Switch<undefined>()(data)
.with({ variant: 'Byte'}, data => log(`Invalid byte '${data}'`))
.with({ variant: 'Bytes'}, data => log(`Invalid bytes '${data}`))
.run()
)
.run()
type T50 = AssertsEqual<typeof T11,undefined>
type T51 = TestAll[]
// lib multi
type Param2<T extends State[]> = {
readonly [K in keyof T]: T[K] extends State<E,infer A> ? A : never
}
// Map Next in a tuple of states and transitions
type Next2<SS extends State[],P extends Param2<SS>> = {
[K in keyof SS]: Next<SS[K] extends State ? SS[K] : never, P[K]>
//fix: I`d like to avoid extends in line up above.
}
type ToTuple<U extends unknown[]> = {
[K in keyof U]: State<"not_exhausted", U[K]>
}
type MultMatch<SS extends State[],R> = {
with: <PP extends Param2<SS>>(pattern: PP, match: (data: PP) => R) => MultMatch<Next2<SS,PP>,R>
when: <PP extends Param2<SS>>(pattern: PP, whenCondition: (data:PP) => boolean, match: (data: PP) => R) => MultMatch<SS,R>
run: () => SS extends State<"exhausted">[] ? R : never
default: (match: (data: Param2<SS>) => R) => MultMatch<SS extends State<"exhausted">[] ? State<"unused">[] : State<"exhausted">[],R> // note: may improve to detect if there is a over-exaustion
}
declare const MultMatch: <U extends unknown[],R>(data: U) => MultMatch<ToTuple<U>,R>
// Lib pre-test
// =================================================================
// Test
// =================================================================
// ----------------------
// use playground
// ----------------------
// single
const y = Match<Q,string>(q)
.with(0, _ => "foo1")
//.when(0, data => data === data, _ => "foo2")
.with(1, _ => "foo3")
.with(2, _ => "foo4")
//.default( data => `foo5 ${data}`)
.run()
// mult
const y2 = MultMatch<[Q,X],string>([q,x])
.with([q0,x1], _ => "foo1")
.when([q1,x0], ([x,y]) => String(x) !== y, _ => "foo2")
//.with([Q1,X1], _ => "foo3")
//.with([Q2,X2], _ => "foo4")
.default( data => `foo5 ${data}`)
.run()
//
// single
const y1_ = Match<Data,string>(data)
.withVariant("NoBytes", _ => `there's no bytes there`)
//.with( {variant: "Byte"}, bytes => `bytes: ${bytes}`)
//.with()
//.with({variant: ""})
//.with({variant: ""}, _ => "foo3")
//.with("NoBytes", _ => "foo4")
//.run()
// mult
const y2__ = MultMatch<[Q,X],string>([q,x])
.with([q0,x1], _ => "foo1")
.with([q1,x2], _ => "foo3")
.with([q2,x0], _ => "foo4")
.run()
//
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment