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.