typescript - Hvordan typer jeg parameteriserer en tuple?


Jeg har en tuple der typene er relatert til hverandre. I mitt tilfelle er det en ekstraksjonsfunksjon som henter en verdi som deretter brukes som input til en annen funksjon.

Konseptuelt sett er det jeg leter etter noe slikt, men dette kompilerer ikke:

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

Brukstilfellet er dette. Jeg har en innkommende RPC-melding av typen any, og en API med velkjente argumenttyper. Jeg vil bygge en "koblingsplan" som tar to argumenter, en utvinningsfunksjon og den tilsvarende API-funksjonen.

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

Jeg kan omgå problemet på andre måter, det slo meg bare at det kan være noe med tupler jeg ikke har forstått.

Svar #1

Konseptuelt sett er det jeg leter etter noe slikt, men dette kompilerer ikke:

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

Det er faktisk det motsatte av det du ønsker. Basert på intuisjonen for funksjonstyper, betyr a: <T>(t: T) => T at du har en funksjon som fungerer for alle typer. Dette er en universell kvantor: implementeringen av a vet ikke hva T er; brukeren av a kan sette T til hva de vil. Å gjøre dette for tuplet ditt ville være katastrofalt, fordi de indre funksjonene må returnere verdier av T uansett hva T er, og derfor er det eneste de kan gjøre å kaste feil/ligge i en uendelig løkke/være bunn på en eller annen måte (de må returnere never).

Du vil ha eksistensiell kvantifisering. a: ∃T. [(v:any) => T, (t:T) => void] betyr at a har en type T knyttet til seg. Implementeringen av a vet hva det er og kan gjøre hva den vil med det, men brukeren av a vet nå ingenting om det. I praksis snur det rollene sammenlignet med universell kvantifisering. TypeScript har ikke støtte for eksistensielle typer (ikke engang i en superenkel form som Java sine wildcards), men det kan simuleres:

type WirePlanEntry = <R>(user: <T>(name: string, reader: (msg: any) => T, action: (t: T) => Promise<any>)) => R
type WirePlan = WirePlanEntry[]

Ja, det er mye å si. Det kan deles opp til:

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

Å konsumere EWirePlanEntry ser slik ut

plan(<T>(eT: WirePlanEntry<T>) => ...)
// without types
plan(eT => ...)

men hvis du bare har forbrukere som

function consume<T>(plan: WirePlanEntry<T>): R // R is not a function of T

du vil bruke dem som

plan(consume) // Backwards!
lift(consume)(plan) // Forwards!

Nå, derimot, kan du ha produsenter. Den enkleste slike produsenten er allerede skrevet: existentialize.

Her er resten av koden din:

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

I aksjon

Relaterte artikler