Hvordan forhindre innsnevring av generiske typer når man bruker literaler med currying i TypeScript?


Jeg har et problem med å designe typer for Sanctuary (js-bibliotek for funksjonell programmering). Jeg ønsker å lage Ord-typen, som representerer en hvilken som helst verdi som har naturlig orden. Ord er enten:

  1. Noen innebygde primitive typer: number, string, Date, boolean
  2. Enhver brukertype som implementerer nødvendig grensesnitt (compareTo-metode)
  3. Et array av Ord

For å uttrykke det brukte jeg (som virker naturlig for meg) unionstype (med en liten løsning for å gjøre gjentakende typer mulig), som:

type Ord = boolean | Date | number | string | Comparable<any> | OrderedArray

interface OrderedArray extends Array<Ord> {}

(enhver kode jeg nevner er tilgjengelig på dette TypeScript-lekeplassen).

Deretter, for å lage typedefinisjoner for f.eks. compare-funksjonen, brukte jeg generisk type med typebegrensning, altså noe som (merk at det er kurret):

function compare<T extends Ord>(a: T): (b: T) => number

Dessverre, mens man bruker en slik tilnærming med litterale argumenter, f.eks. (compare(2)(3), som er helt gyldig kode), gir TypeScript en feil, med '3' kan ikke tildeles til '2'. Det ser ut til at når det første argumentet er gitt, innsnevres TypeScripts typeparameter til 2 (litteraltypen som bare inneholder verdien 2), mens den egentlig bare skulle innsnevres til number. Finnes det en måte å forhindre at den gjør dette på? Eller noen annen fungerende tilnærming?

Svar #1

Du kan også bruke typesystemet til å gjøre den bokstavelige utvidelsen selv hvis kompilatoren ikke gjør det (som i dette tilfellet der T har en kontekstuelt type som inkluderer string, number og boolean):

type Widen<T> = 
  T extends string ? string : 
  T extends number ? number : 
  T extends boolean ? boolean : 
  T;

const compare = <T extends Ord>(x: T) => (y: Widen<T>) => 0;

Så x vil forbli smal, men y vil bli utvidet. Dette vil tillate følgende kall:

compare(1)(3);  // <1>(x: 1) => (y: number) => number

Håper det hjelper; lykke til!

Lenke til kode

OPPDATERING: hvis du virkelig vil kunne beholde T smal valgfritt, kan du gjøre følgende kompliserte ting:

type OptionallyWiden<X, T> = 
   [X] extends [never] ? 
     T extends string ? string : 
     T extends number ? number : 
     T extends boolean ? boolean : 
     T
  : T;

const compare = <X extends Ord = never, T extends Ord = X>(x: T) => (
  y: OptionallyWiden<X, T>
) => 0;

som gir deg denne oppførselen:

compare(1)(3); // okay
// const compare: <never, 1>(x: 1) => (y: number) => number

compare<1 | 2>(1)(3); // error now
//  ------------> ~
// 3 is not assignable to 1 | 2
// const compare: <1 | 2, 1 | 2>(x: 1 | 2) => (y: 1 | 2) => number

Det fungerer ved å bruke to typeparametere... den første, X, vil standard til never med mindre det spesifiseres manuelt. Hvis X er never, vil den utledede verdien av T bli bredere. Hvis X ikke er never, vil den spesifiserte verdien bli brukt og ikke bredere.

Er fleksibiliteten her verdt den rare, klønete typemanipulasjonen? Jeg antar det er opp til deg å finne ut av det, men jeg ville sannsynligvis bare beholdt den originale <T extends Ord>(x: T) => (y: T) => 0 hvis jeg ønsket fleksibilitet til å ha både smale og brede T-verdier, og bare manuelt utvidet T hvis det er det jeg vil.

Uansett, håper dette hjelper igjen!

Lenke til kode

Svar #2

Jeg fant det til slutt ut (i det minste på en måte som fungerer, ikke nødvendigvis den beste). Det viste seg at å legge til eksplisitte overbelastninger for alle typer som har bokstavelig representasjon, og derfor kunne begrenses, fikser det, slik som:

function compare(x: number): (y: number) => number;
function compare(y: string): (y: string) => number;
function compare(x: boolean): (y: boolean) => number;

// and generic overload
function compare<T extends Ord>(x: T): (y: T) => number;
function compare<T extends Ord>(x: T): (y: T) => number {
  ...
};

Jeg tror disse eksplisitte overbelastningene har forrang i signaturoppløsning, og dermed er problemet mitt borte, som:

compare(2)(3) // resolves to compare(x: number)(y: number)

Løsningen er tilgjengelig på TypeScript-lekestedet.

Relaterte artikler