Reputation: 13620
I have a tuple where the types relate to each other. In my case it's an extractor function that extracts a value that is in turn used as input to another function.
Conceptually what I'm looking for is something like this, but this doesn't compile:
const a: <T>[(v:any) => T, (t:T) => void] = [ ... ]
The use case is this. I have an incoming RPC message of type any
, and an API with well 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's 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 work around the issue in other ways, it just struck me there may be something about tuples I haven't understood.
Upvotes: 5
Views: 1542
Reputation: 29193
Conceptually what I'm looking for is something like this, but this doesn't compile:
const a: <T>[(v:any) => T, (t:T) => void] = [ ... ]
That is, in fact, the opposite of what you want. Drawing on the intuition of function types, a: <T>(t: T) => T
means you have a function that works for all types. This is a universal quantifier: the implementation of a
doesn't know what T
is; the user of a
can set T
to whatever they want. Doing this for your tuple would be disastrous, as the inner functions need to output values of T
no matter what T
is, and therefore the only thing they can do is error out/loop forever/be bottom in some way or another (they must return never
).
You want existential quantification. 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 the user of a
now knows nothing about it. In effect, it reverses the roles when compared to universal quantification. TypeScript doesn't have support for existential types (not even in a super basic form like Java's wildcards), but it can be simulated:
type WirePlanEntry = <R>(user: <T>(name: string, reader: (msg: any) => T, action: (t: T) => Promise<any>)) => R
type WirePlan = WirePlanEntry[]
Yes, that is a mouthful. It can be decomposed to:
// 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.
type EWirePlanEntry = <R>(consumer: WirePlanEntryConsumer<R>) => R
// this is an application of the fact that the statement
// "there exists a T for which the statement P(T) is true"
// implies that
// "not for every T is the statement P(T) false"
// 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) => plan(consumer)
}
Consuming EWirePlanEntry
looks like
plan(<T>(eT: WirePlanEntry<T>) => ...)
// without types
plan(eT => ...)
but if you just have consumers like
function consume<T>(plan: WirePlanEntry<T>): R // R is not a function of T
you'll use them like
plan(consume) // Backwards!
lift(consume)(plan) // Forwards!
Now, though, you can have producers. The simplest such producer has already been written: existentialize
.
Here's 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: any) => {
let entry = wirePlan.find(lift((w) => w[0] === msg.name))
if(entry) {
entry(handler => {
const extractedValue = handler[1](msg)
return handler[2](extractedValue)
})
}
}
Upvotes: 6