Hvordan kan jeg sende en generell verditype som nås gjennom en indeks i TypeScript?


Jeg prøver å sende en generell verdi til en type som aksesseres gjennom en indeks, men jeg støter på en syntaksfeil.

Jeg har en funksjonstype-signatur kalt onCreateNode definert på et grensesnitt kalt GatsbyNode:

interface GatsbyNode {
  ...
  onCreateNode?<TNode extends object = {}>(
    args: CreateNodeArgs<TNode>,
    options?: PluginOptions,
    callback?: PluginCallback
  ): void
  ...

Jeg har nå laget en funksjon som også heter onCreateNode, og jeg vil tilordne den typen GatsbyNode['onCreateNode'] samtidig som jeg sender en verdi til TNode-generikken:

export const onCreateNode: GatsbyNode['onCreateNode']<AGenericTypeIWantToPassIn> = ...

Jeg får imidlertid en syntaksfeil ved den første vinkelparentesen (<) som forteller meg at dette ikke er gyldig TypeScript.

Svar #1

I TypeScript er det ikke mulig å bruke typeargumenter på indekserte tilgangstyper. Det er med vilje fordi det ville kreve å utvide grammatikken for å støtte flere sett med typeargumenter for generiske typer med generiske metoder, som for eksempel

type Foo<T> = <U>(t: T, u: U) => [T, U];

Dermed vil ikke følgende fungere:

class Echo {
  echo = <T>(a: T): T => a;
}
type EchoNumFunc = Echo["echo"]<number>; // Does not work

TypeScript 4.7 introduserte instansieringsuttrykk som gir muligheten til å spesifisere typeargumenter for generiske funksjoner eller generiske konstruktører uten å faktisk kalle dem:

function makeBox<T>(value: T) {
    return { value };
};
const makeStringBox = makeBox<string>;  // (value: string) => { value: string }
const stringBox = makeStringBox('abc');  // { value: string }

Å utnytte nøkkelordet declare lar deg dra nytte av den tidligere nevnte funksjonen for å omgå problemet på følgende måte:

class Echo {
  echo = <T>(a: T): T => a;
}
declare const echo: Echo["echo"];
type EchoNumFunc = typeof echo<number>; // Works

Svar #2

Du prøver å konvertere en generisk funksjon-type til en generisk type som refererer til en funksjon. Dette er to fundamentalt forskjellige typer generiske, og selv om de er relatert, gir ikke kompilatoren deg egentlig en direkte mekanisme for å konvertere mellom dem. Se dette svaret for en mer fullstendig forklaring; dette spørsmålet spør i bunn og grunn om det samme som det andre spørsmålet spør om.

Uansett, den mest direkte måten for deg å gå frem på er å bare skrive om typedefinisjonen i ønsket form selv:

type GatsbyNodeSpecific<T extends object = {}> = {
  onCreateNode?(args: CreateNodeArgs<T>, options?: PluginOptions, callback?: PluginCallback): void;
}

export const onCreateNode: GatsbyNodeSpecific<AGenericTypeIWantToPassIn>['onCreateNode'] = null!

Dette vil definitivt fungere, men kan være tidkrevende og potensielt feilutsatt hvis den opprinnelige definisjonen av GatsbyNode endres.


I det andre svaret viser jeg en jukseteknikk for å overbevise kompilatoren om å ta en generisk funksjonstype og konvertere den til en generisk type. For ditt tilfelle ville det se slik ut:

class GenTypeMaker<T extends object = {}> {
  getGatsbyNodeSpecific!: <A extends any[], R>(cb: (...a: A) => R) => () => (...a: A) => R;
  gatsbyNodeSpecific = this.getGatsbyNodeSpecific!(
    null! as NonNullable<GatsbyNode["onCreateNode"]>)<T>()
}
type OnCreateGatsbyNodeSpecific<T extends object> = 
  GenTypeMaker<T>['gatsbyNodeSpecific'] | undefined;

Og så kan du bruke denne nye typen på denne måten:

export const onCreateNode2: OnCreateGatsbyNodeSpecific<AGenericTypeIWantToPassIn> = onCreateNode;

Dette er den samme typen som før. Men jeg tviler på at noen som gjennomgår koden over med en fantom class vil være fornøyd med det, og den enkle, om enn overflødige, metoden med å skrive om definisjonen manuelt er sannsynligvis den bedre fremgangsmåten.

Ok, håper det hjelper; lykke til!

Lenke til kode på lekeplassen

Relaterte artikler