1
votes

Types conditionnels Typescript déduits par une fonction d'ordre supérieur

J'ai une fonction qui peut renvoyer un résultat de synchronisation ou asynchrone

// all handlers are sync, infer boolean
const allHandlersAreOk: boolean = myClassSync.invokeHandlers()

// all handlers are async, infer Promise<boolean>
const allAsyncHandlersAreOk: Promise<boolean> = await myClassAsync.invokeHandlers()

// at least one handler is async, infer Promise<boolean>
const allMixedHandlersAreOk: Promise<boolean> = await myClassMix.invokeHandlers()

et une classe qui prend une liste de ces fonctions

const myClassSync = new MyClass<MyType>([
   (ctx) => true,
   (ctx) => false
]);

const myClassAsync = new MyClass<MyType>([
   async (ctx) => Promise.resolve(true),
   async (ctx) => Promise.reject()
]);

const myClassMix = new MyClass<MyType>([
   async (ctx) => Promise.resolve(true),
  (ctx) => true
]);

Je me demandais s'il y avait une chance de faire en sorte que dactylographié infère le type de retour du invokeHandlers () basé sur les gestionnaires donnés. Considérez que tous les gestionnaires sont déclarés au moment du design:

class MyClass<T> {

    constructor(private handlers: Array<HookHandler<T>>) {

    }

    public invokeHandlers() : boolean | Promise<boolean> {
        // invoke each handler and return:
        // - Promise<boolean> if exist a handler that return a Promise<T>
        // - boolean if all handlers are synchronous
    }

}

Puis-je rendre le type de retour de invokeHandlers () dépendant des types du courant donné des hanlders sans casting explicite? Donc par exemple

type HookHandler<T> = (context: MyClass<T>) => boolean | Promise<boolean>;

Je peux évidemment renvoyer un simple Promise , mais je perdrais la possibilité d'appeler les invokeHandlers ( ) dans des contextes synchrones, et il veut éviter cela.

Des suggestions ou un autre choix de conception pour faire face au problème? Merci!


2 commentaires

J'ai peut-être une solution pour vous, mais votre code n'a aucune dépendance structurelle du paramètre T , donc je prévois de l'exclure.


Le type générique est copié à partir du code réel, ce n'est qu'une simplification


3 Réponses :


0
votes

Que certains d'entre eux puissent retourner des promesses est un fait. C'est tout ce que TypeScript peut savoir.

Si elles sont ou non toutes les promesses de retour ne peuvent être déterminées qu'au moment de l'exécution.

La réponse est donc non, TypeScript ne peut pas déduire quelque chose qui ne peut être déduit qu'au moment de l'exécution.


3 commentaires

Logique. Pourrait être une solution pour vous de diviser le invokeHandlers () en deux fonctions, l'une qui renvoie un booléen et l'autre renvoyant un code Promise ?


Oui, mais alors celui qui ne peut renvoyer que boolean devra être tapé de telle manière que ses paramètres ne puissent jamais être des promesses, vous obligeant (en tant que programmeur) à utiliser le bon dès le début . Et cela supprimerait fondamentalement l'avantage de pouvoir mélanger les gestionnaires async et sync. OU vous pouvez effectuer la vérification au moment de l'exécution au lieu de la compilation, mais vous perdez alors les avantages de TypeScript.


Oui, c'était le but. Actuellement, j'ignore les gestionnaires asynchrones si l'appelant utilise la version de synchronisation de invokeHandlers () , mais je cherchais une solution plus propre



1
votes

vous pouvez utiliser des surcharges si vous avez un moyen de différencier vos gestionnaires ou de les identifier d'une manière ou d'une autre au moment de l'exécution

function handler(context: AsyncHandler): Promise<boolean>;
function handler(context: MixedHandlers): Promise<boolean>;
function handler(context: SyncHandlers): boolean:
function handler(context){
  // your implementation, maybe instanceof if each type has a class representation
}

Donc si vous pouviez écrire quelque chose comme:

function handler(x: number): string;
function handler(y: string): number;
function handler(arg) {
    if (typeof arg === 'number') {
        return `${arg}`
    } else {
        return parseInt(arg);
    }
}

const inferred = handler(1); // <-- typescript correctly infers string
const alsoInferred = handler('1'); // <-- typescript correctly infers number

TypeScript pourrait correctement déduire le type de retour. Je ne sais pas si cela est possible en fonction de votre structure de code, mais j'ai pensé que je partagerais. En savoir plus ici , en particulier la section sur les surcharges

p >


1 commentaires

Merci, je vais essayer



1
votes

Voici comment je l'aborderais:

Trouvez des types séparés pour chaque gestionnaire de hook possible:

const myClassSync = new MyClass([
  (ctx) => true,
  (ctx) => false
]);
// all handlers are sync, infer boolean
const allHandlersAreOk: boolean = myClassSync.invokeHandlers()

const myClassAsync = new MyClass([
  async (ctx) => Promise.resolve(true),
  async (ctx) => Promise.reject()
]);
// all handlers are async, infer Promise<boolean>
// note you do not "await" it, since you want a Promise
const allAsyncHandlersAreOk: Promise<boolean> = myClassAsync.invokeHandlers()

const myClassMix = new MyClass([
  async (ctx) => Promise.resolve(true),
  (ctx) => true
]);
// at least one handler is async, infer Promise<boolean>
// note you do not "await" it, since you want a Promise
const allMixedHandlersAreOk: Promise<boolean> = myClassMix.invokeHandlers()

Et puis créez MyClass code > dépendent du type HH de HookHandler que vous utilisez. Le type de retour de invokeHandlers peut être un type conditionnel qui évalue en booléen si HH est SyncHookHandler , et Promise si HH est AsyncHookHandler ou AsyncHookHandler | SyncHookHandler :

class MyClass<HH extends HookHandler> {

  constructor(private handlers: Array<HH>) { }

  public invokeHandlers(): Promise<boolean> extends ReturnType<HH> ? 
    Promise<boolean> : boolean;
  public invokeHandlers(): boolean | Promise<boolean> {

    const rets = this.handlers.map(h => h(this));

    const firstPromise = rets.find(r => typeof r !== 'boolean');
    if (firstPromise) {
      return firstPromise; // 🤷‍ what do you want to return here
    }
    // must be all booleans
    const allBooleanRets = rets as boolean[];
    return allBooleanRets.every(b => b);  // 🤷‍ what do you want to return here 
  }
}

Je viens de faire une implémentation idiote dans invokeHandlers () pour donner une idée de ce que vous y feriez . Vous pouvez maintenant voir que votre code se comporte comme prévu

type SyncHookHandler = (context: MyClass<any>) => boolean;
type AsyncHookHandler = (context: MyClass<any>) => Promise<boolean>;
type HookHandler = AsyncHookHandler | SyncHookHandler;

Est-ce que cela fonctionne pour vous?

Veuillez noter que puisque l'exemple de code n'avait aucune dépendance structurelle sur le paramètre générique T , j'ai l'a supprimé . Si vous en avez besoin, vous pouvez le rajouter aux endroits appropriés, mais je suppose que la question concerne davantage la détection-sync-si-vous-pouvez-et moins un type générique.

D'accord, j'espère que cela vous aidera; bonne chance!


0 commentaires