2 votes

TypeScript : surcharge de type de retour basée sur le nombre de paramètres de callback

Contexte :

Je tente de créer une petite aide qui enveloppera les composants React soit avec memo(fn) soit avec memo(forwardRef(fn) en fonction de la présence éventuelle du paramètre facultatif ref dans la définition du composant. Donc en gros :

Test((props)=>{}) // même chose que : memo((props)=>{})
Test((props, ref)=>{}) // même chose que : memo(forwardRef(props, ref)=>{});

Problème :

Mis à part les détails de React, considérons cet exemple minimal :

type AnyObject = Record;
type EmptyObject = Record;
type RenderResult = null; // `React.ReactElement | null` dans le monde réel
type RefType = string; // `React.ForwardedRef` dans le monde réel

function Test(
    Component: (props: Props) => RenderResult
): (props: Props) => RenderResult;

function Test(
    Component: (props: Props, ref: RefType) => RenderResult
): (props: Props & { ref: RefType }) => RenderResult;

function Test(Component: (...args: any[]) => RenderResult) {
    // TODO: memo() / memo(forwardRef()) basé sur Component.length
    return Component;
}

type Props = { a: string };

// BIEN : typeof x === (props: Props) => null
// BIEN : typeof props === Props
const x = Test((props) => null);

// BIEN : typeof y === (props: Props & {ref: RefType}) => null
// MAUVAIS : typeof props === any
// MAUVAIS : typeof ref === any
const y = Test((props, ref) => null);

Ça fonctionne presque, mais pas tout à fait. Si vous réorganisez la définition des surcharges de Test, le résultat est légèrement différent, mais toujours pas ce que je veux (ref serait présent à la fois dans x et y).

J'ai une vague idée de pourquoi cela pourrait se produire (trop de chevauchements ? aucun moyen pour TS de distinguer ces deux cas ?), mais je n'ai aucune idée de comment le résoudre.

1voto

jcalz Points 30410

Vous avez rencontré une limitation de conception de TypeScript comme décrit dans microsoft/TypeScript#11936. Le type (props: X) => Z est intentionnellement assignable au type (props: X, ref: Y) => Z, donc tout code qui tente de les distinguer pourrait être délicat. Apparemment, l'algorithme de résolution des surcharges se heurte à leur similarité et échoue à typiser contextuellement vos paramètres de rappel. Tant pis.


Vous devrez donc contourner cela. Le contournement général pour l'échec d'inférence des paramètres de fonction contextuellement est de les annoter explicitement vous-même, comme

Test((props: Props, ref: RefType) => null);

mais si cela ne vous convient pas, vous devrez trouver un autre contournement.


Un contournement lorsque les surcharges ne fonctionnent pas pour les appelants est de rendre la fonction générique et d'utiliser des types conditionnels dans le type de retour. Quelque chose comme

function foo(...args: Args1): Ret1;
function foo(...args: Args2): Ret2;
function foo(...args: Args3): Ret3;

peut être transformé en quelque chose comme

function foo(...args: A): 
  A extends Args1 ? Ret1 : A extends Args2 ? Ret2 : Ret3;

Ainsi, lorsque vous appelez la nouvelle fonction foo(), le compilateur infère A à partir des arguments, et le type de retour est choisi en fonction de cela.

Malheureusement, vous utilisez déjà des génériques dans vos surcharges, et pire encore, vous spécifiez manuellement le paramètre de type lorsque vous l'appelez. La nouvelle fonction générique devrait avoir plusieurs arguments de type, l'un d'entre eux étant spécifié manuellement, et l'autre devant être inféré. Mais TypeScript ne prend pas en charge l'inférence partielle des arguments de type, comme décrit dans microsoft/TypeScript#26242. Consultez Typescript: infer type of generic after optional first generic. Donc, pour que cela fonctionne, nous devons contourner cette limitation.

Le contournement le plus propre consiste à utiliser la curryfication et à diviser la fonction à paramètres multiples en deux fonctions à paramètre unique, de sorte que vous spécifiez manuellement l'argument de type lors de l'appel de la première fonction, et celle-ci renvoie une seconde fonction que vous appelez et que le compilateur infère l'argument de type pour celle-ci. Cela ajoute un appel de fonction supplémentaire pour les appelants, ce qui n'est pas idéal, mais ce ne sont que quelques parenthèses supplémentaires (par exemple, au lieu de f(x, y) vous devez écrire f()(x, y)).

Pour votre exemple, cela ressemble à quelque chose comme :

function Test

Donc vous appelez Test(), et cela renvoie une fonction de type

 RenderResult>(
    Component: F
) => (props: Props & (
    Parameters['length'] extends 0 | 1 ? unknown : { ref: RefType })
) => RenderResult;

Et vous pouvez voir que la fonction infère F à partir de Component, et si la liste des paramètres de F est inférieure à deux arguments, alors la valeur retournée est de type (props: Props) => RenderResult. Sinon, elle est de type (props: Props & {ref: RefType) => RenderResult.

Notez que cela signifie que l'implémentation de Test() doit également être modifiée pour être curryfiée (à l'exécution, cela équivaut simplement à retourner un thunk) :

function Test() {
    return function (Component: (...args: any[]) => RenderResult) {
        // TODO: memo() / memo(forwardRef()) basé sur la longueur de Component
        return Component;
    }
}

Eh bien, testons :

type Props = { a: string };

const x = Test()(props => null);
// const x: (props: Props) => RenderResult

const y = Test()((props, ref) => null);
// const y: (props: Props & { ref: RefType; }) => RenderResult

Cela fonctionne ! Ces parenthèses supplémentaires sont là, mais vous obtenez tous les types que vous recherchez.


Donc voilà. Peut-être que ce contournement répondra à vos besoins, mais sinon, vous devrez probablement vous contenter de quelque chose d'autre qui ne fait pas exactement ce que vous voulez... du moins jusqu'à ce qu'il y ait éventuellement un changement dans le code de résolution des surcharges comme décrit dans microsoft/TypeScript#11936.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X