Conceptually, what I'm looking for is something like this, but this does not compile:
const a: <T>[(v:any) => T, (t:T) => void] = [ ... ]
That is, in fact, the opposite of what you want. Based on the intuition of function types, a: <T>(t: T) => T means that you have a function that works for all types. This is a universal quantifier: the implementation of a does not know what T ; user a can set T to whatever he wants. Doing this for your tuple would be disastrous, since the inner functions need to output the values ββof T no matter what T , and therefore the only thing they can do is error / loop forever / be bottom in some way or another (they should return never ).
You want an existential quantification . a: βT. [(v:any) => T, (t:T) => void] a: βT. [(v:any) => T, (t:T) => void] means that a has some type T associated with it. The implementation of a knows what it is and can do whatever it likes with it, but user a now knows nothing about it. In fact, it changes roles compared to universal quantitative assessment. TypeScript does not support existential types (even in a super basic form, such as Java templates), but this can be modeled :
type WirePlanEntry = <R>(user: <T>(name: string, reader: (msg: any) => T, action: (t: T) => Promise<any>)) => R type WirePlan = WirePlanEntry[]
Yes, this is a sip. It can be decomposed into:
// Use universal quantification for the base type type WirePlanEntry<T> = [string, (msg: any) => T, (t: T) => Promise<any>] // A WirePlanEntryConsumer<R> takes WirePlanEntry<T> for any T, and outputs R type WirePlanEntryConsumer<R> = <T>(plan: WirePlanEntry<T>) => R // This consumer consumer consumes a consumer by giving it a `WirePlanEntry<T>` // The type of an `EWirePlanEntry` doesn't give away what that `T` is, so now we have // a `WirePlanEntry` of some unknown type `T` being passed to a consumer. // This is the essence of existential quantification. // Note: We're essentially using callbacks, and therefore summoning callback hell. type EWirePlanEntry = <R>(consumer: WirePlanEntryConsumer<R>) => R // Convert one way function existentialize<T>(e: WirePlanEntry<T>): EWirePlanEntry { return <R>(consumer: WirePlanEntryConsumer<R>) => consumer(e) } // Convert the other way function lift<R>(consumer: WirePlanEntryConsumer<R>): (e: EWirePlanEntry) => R { return (plan: EWirePlanEntry) => { return plan(consumer) } }
EWirePlanEntry consumption looks like
e(<T>(eT: WirePlanEntry<T>) => ...) // === plan(eT => ...)
but if you only have consumers like
function consume<T>(plan: WirePlanEntry<T>): R
you will use them as
plan(consume) // Backwards! lift(consume)(plan) // Forwards!
Now, however, you may have producers. The simplest such manufacturer has already been written: existentialize .
Here is the rest of your code:
type WirePlan = EWirePlanEntry[] const wirePlan: WirePlan = [ existentialize(['saveModel', (msg:any) => <Model>msg.model , api.saveModel]), existentialize(['getModel' , (msg:any) => <string>msg.modelID, api.getModel ]), ] const handleMessage = (msg) => { return wirePlan.find(lift((w) => w[0] === msg.name))(handler => { const extractedValue = handler[1](msg) return handler[2](extractedValue) }) // Ew, callback hell. }
In action
Full disclosure: I really don't know TypeScript. This is something prepared from the above sources and my knowledge of Scala and its type system. Secondly, the TypeScript playground seems rather happy when this code is in it; I'm not sure if this is just a thing that tends to do, or if this code does the compiler in some way (it compiles, though), but there it is.