Typescript-type med ikke mer enn 10 unike egenskaper, eller en oppføringsbasert ekvivalent tuple på lengde 0 - 10


Jeg skriver noe generisk wrapper-kode over Javascript AWS SDK. SDK-en er ganske forferdelig på noen måter som jeg gjerne vil gjøre litt mindre smertefulle.

SQS tillater å sende inn noen meldingsattributter (MessageBodyAttributeMap), som for formålet med dette spørsmålet, skal vi late som er typet som { [key: string]: Buffer | Uint8Array | Blob | string } (dette er en del av sukkeret jeg håper å legge til).

Men en uuttrykt begrensning her er at det bare kan være 10 egenskaper i dette meldingsattributtobjektet, og det virker umulig å håndheve et spesifikt antall objekt-egenskaper i TypeScript. Det finnes imidlertid noe som kan håndheve lengde, nemlig en tuppel av oppføringer som jeg lett kunne konvertere til et objekt ved kjøring:

type MessageValue = Buffer | Uint8Array | Blob | string
type Entry<
  TKey extends string,
  TValue extends MessageValue
> = TKey extends never
  ? never
  : TValue extends never
    ? never
    : [TKey, TValue]

type EntryTuple<
  TEntry1 extends Entry<string, MessageValue> = never
  TEntry2 extends Entry<string, MessageValue> = never
  // T3 - T9
  TEntry10 extends Entry<string, MessageValue> = never
> = [TEntry1, TEntry2, /* T3 - T9 */, TEntry10]

Teoretisk sett kunne en konkret DurableQueue-klasse som aksepterer TEntry1 - TEntry10 generiske typeargumenter, ved konstruksjonstid låse inn et bestemt sett med oppføringer slik at typesystemet ville håndheve AttributeMap-typen på både innkommende og utgående meldinger. Jeg ville være fornøyd med å akseptere et objekt, men også et faktisk sett med oppføringer som er egnet for Object.fromEntries() og returnere oppføringer ved hjelp av Object.toEntries().

Men det er noen alvorlige problemer.

  1. Typer kan alltid være en union. Som en tuple er det ingen garanti for at hver oppføring-nøkkel er en enkelt strengkonstant.
  2. Akkurat nå sliter jeg også med å finne ut hvordan jeg kan få hver verdi til å være av en bestemt type slik at de ikke varierer per tuppeloppføring.
  3. Object.toEntries() har ingen garanti for rekkefølgen på nøklene. Kanskje jeg kunne løse det ved at klassen min aksepterer en entry-sorterings callback og lar konstruktøren sikre at sorteringen vil samsvare med rekkefølgen på utgangs-tupletypen?
  4. Unikhet kommer til å bli et problem. Jeg kan kanskje bruke noen smarte rekursive typer for å slå sammen alle TEntry1 - TEntry10-nøkler og sikre at inndataantallet samsvarer med det sammenlagte (og dedupliserte) nøkkelantallet.

Hvordan kan jeg utsette spesifiseringen av min pseudo-MessageBodyAttributeMap til konstruksjonstid gjennom generiske typeargumenter?

For å prøve å gjøre dette mer konkret, hvis jeg skulle statisk typisere klassen min slik at metoden queue() under hadde én nøyaktig AttributeMap-definisjon (som metode vil kalle SQS.sendMessage()), kan det se slik ut, hvor TypeArguments representerer de ukjente typeparametrene jeg måtte bruke for å oppnå målet:

class SqsQueue<TypeParameters> implements IDurableQueue<TypeParameters> {
  private readonly _sqs: SQS
  constructor<TypeParameters>(sqs: SQS) {
    this._sqs = sqs
  } 
  async queue(
    body: string,
    attributeMap: [
      ['attribute1', Uint8Array],
      ['attribute2', string],
      ['attribute3', Buffer]
    ] // the concrete type of up to 10 entries 
    // or an equivalent object with no more than 10 properties
    // that was actually specified by the new() call's type arguments
  ) {
    const result = await this._sqs.sendMessage({
      MessageAttributes: convertAttributeMapToSqsMessageAttributes(attributeMap),
      ...
    })
    ...
    return transformedResult
  }
}

Det vil si, når vi instansierer ovennevnte klasse:

const myQueue: IDurableQueue<...> =
  new SqsDurableQueue<
    [
      ['attribute1', Uint8Array],
      ['attribute2', string],
      ['attribute3', Buffer]
    ]
  >()

typeargumentene ville være i formen vist, men ville være påkrevd av klassens typeparameterbegrensninger for å:

  1. være en tuple mellom 0 og 10 elementer lang
  2. bruk unike streng-oppføringsnøkler
  3. og garantere at hver oppføringsverdi kun vil være en av Buffer | Uint8Array | Blob | string

Jeg vet at dette er et komplisert spørsmål, og det kan være et langt skudd å spørre om. Kanskje etter å ha sovet på det vil jeg få litt inspirasjon (det pleier av og til å skje). I mellomtiden sitter jeg fast med hvordan jeg praktisk kan tillate opprettelsen av en DurableQueue-klasse som i praksis (om ikke i realiteten) lar meg definere et objekt med ikke mer enn 10 unike egenskaper som beskrevet.

Spørsmålet mitt virker ganske dårlig; jeg har definitivt problemer med å forklare kort hva jeg prøver å gjøre. Beklager for det, og til alle lesere som kommer så langt, jeg har stor takknemlighet!

P.S. Jeg kom akkurat på mixins, men har faktisk aldri brukt dem med suksess. Jeg har bare lekt med dem innimellom. Er ikke sikker på hvordan det skulle hjelpe med å løse ting. Jeg lurer på om det å bruke et «malobjekt» i en mixin-fabrikkfunksjon (for å utlede MessageAttributes-typen fra) kunne hjelpe til med å lage en type som i det minste, ved konstruksjonstidspunktet, kaster en feil hvis malobjektet har mer enn 10 egenskaper?