How do I type parameterize a tuple?

后端 未结 1 416
生来不讨喜
生来不讨喜 2020-12-10 18:19

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.

相关标签:
1条回答
  • 2020-12-10 18:53

    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)
        })
      }
    }
    

    In Action

    0 讨论(0)
提交回复
热议问题