reactjs - Kan jag använda villkorlig generisk för att sätta en callback-returtyp i TypeScript?


Jag har en React-komponent till vilken jag skickar en generisk typ. Baserat på denna generiska typ vill jag ändra typen på callback-payloaden.

I exemplet nedan skickar jag RelationType som kan vara 'one' eller 'many' och baserat på detta bör callbacken vara antingen en sträng eller en array av strängar.

import React from 'react';

export enum RelationType {
  one = 'one',
  many = 'many',
}

interface Props<R extends RelationType> {
  callback(newValue?: R extends RelationType.one ? string : string[]): void;
  relationType: R;
}

export const Component = <R extends RelationType>({ onUpdate, relationType }: Props<R>) => {
  return (
    <button onClick={() => {
      if (relationType === RelationType.one) {
        callback('foo'); // TS2345: Argument of type '"foo"' is not assignable to parameter of type '(R extends RelationType.one ? string : string[]) | undefined'.
      } else {
        callback(['foo', 'bar']); // TS2345: Argument of type 'string[]' is not assignable to parameter of type 'R extends RelationType.one ? string : string[]'.
      }
    }}/>
  )
};

Är detta möjligt med Typescript?

Svar #1

Den generiska typparametern R i Props-typen och Component-funktionen hjälper dig inte. Inuti Component() försöker du kontrollera relationType för att begränsa callback, men generiska typparametrar som R begränsas aldrig via kontrollflödesanalys i TypeScript. Se microsoft/TypeScript#24085 för mer information.

Istället skulle det vara bäst att bara använda en icke-generisk Props av en diskriminerad unionstyp, motsvarande din ursprungliga Props<RelationType.one> | Props<RelationType.many>:

type Props = { [R in RelationType]: {
  callback(newValue?: R extends RelationType.one ? string : string[]): void;
  relationType: R;
} }[RelationType];

/* type Props = {
    callback(newValue?: string | undefined): void;
    relationType: RelationType.one;
} | {
    callback(newValue?: string[] | undefined): void;
    relationType: RelationType.many;
} */

I ovanstående har jag låtit kompilatorn beräkna den unionen programmässigt genom att mappa din ursprungliga Props<R>-definition över RelationType för att skapa en objekttyp vars egenskaper jag omedelbart slår upp.


Då kan din Component()-funktion använda en parameter av typen Props. En annan varning är att du inte kan destrukturera den till relationType och callback inuti implementationssignaturen om du vill att kompilatorn ska hålla koll på relationen mellan de två värdena. TypeScript har inte stöd för det jag har kallat korrelerade unionstyper (se microsoft/TypeScript#30581). Du behöver behålla båda värdena som egenskaper i en enda props-parameter om du vill ha det beteende du söker:

const Component = (props: Props) => {
  return (
    <button onClick={() => {
      if (props.relationType === RelationType.one) {
        props.callback('foo');
      } else {
        props.callback(['foo', 'bar']);
      }
    }} />
  )
};

Det fungerar som önskat, tror jag.

Playground-länk till kod