How do I enter tuple parameterization?

I have a tuple where the types relate to each other. In my case, this is an extractor function that retrieves a value, which in turn is used as input to another function.

Conceptually, what I'm looking for is something like this, but this does not compile:

const a: <T>[(v:any) => T, (t:T) => void] = [ ... ] 

This is an example use. I have an incoming RPC message of type any and an API with known argument types. I want to build a "wiring plan" that takes two arguments, one extractor function and the corresponding API function.

 export interface API = { saveModel : (model:Model) => Promise<boolean>, getModel : (modelID:string) => Promise<Model>, } const api: API = { ... } // this is the tuple type where i'd like to define that // there a relation between the second and third member // of the tuple. type WirePlan = [[string, (msg:any) => T, (t:T) => Promise<any>]] const wirePlan: WirePlan = [[ ['saveModel', (msg:any) => <Model>msg.model , api.saveModel], ['getModel' , (msg:any) => <string>msg.modelID, api.getModel], ] const handleMessage = (msg) => { const handler = wirePlan.find((w) => w[0] === msg.name) const extractedValue = handler[1](msg) return handler[2](extractedValue) } 

I can get around the problem in other ways, it just hit me, maybe something about tuples that I did not understand.

+2
typescript
source share
1 answer

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.

+2
source share

All Articles