Typescript conditional types inferred by high order function

typescript higher order functions
typescript higher order generics
typescript higher rank polymorphism
typescript function
typescript check type
typescript unknown type guard
typescript array of unknown type
typescript tuple type

I have a function that can return a sync or async result

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

and a class that takes a list of that functions

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
    }

}

I was wondering if there is any chance to make typescript infer the return type of the invokeHandlers() based on the given handlers. Consider that all the handlers are declared at design time:

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
]);

Can I make the return type of invokeHandlers() dependent of the types of the current given hanlders without an explicit casting? So for example

// 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()

I can obviously return a simple Promise<boolean>, but I would loose the possibility to call the invokeHandlers() in synchronous contexts, and it want to avoid that.

Any suggestions or other design choice to face the problem? Thank you!

Here's how I'd approach it:

Come up with separate types for each possible hook handler:

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

And then make MyClass depend on the type HH of HookHandler you use. The return type of invokeHandlers can be a conditional type which evaluates to boolean if HH is SyncHookHandler, and Promise<boolean> if HH is AsyncHookHandler or 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 
  }
}

I just did some silly implementation inside invokeHandlers() to give an idea of what you'd be doing there. Now you can see that your code behaves as expected

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()

Does that work for you?

Please note that since the example code had no structural dependence on the generic parameter T, I've removed it. If you need it you can add it back in the appropriate places, but I'm assuming the question is more about detect-sync-if-you-can and less about some generic type.

Okay, hope that helps; good luck!

Higher order function type inference � Issue #30215 � microsoft , Infer higher order function types when possible There is a long way to go until TypeScript reaches the cryptic level of Haskell, deferring function type arguments in type inference through conditional and/or mapped types. Conditional Types in TypeScript January 9, 2019. TypeScript 2.8 introduced conditional types, a powerful and exciting addition to the type system. Conditional types let us express non-uniform type mappings, that is, type transformations that differ depending on a condition.

you could use overloads if you have a way to differentiate between your handlers or identify them in some way at runtime

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

So if you could write something like:

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
}

TypeScript could correctly infer the return type. I'm not sure if this is possible based on your code structure but I thought I would share. Read more here, specifically the section on overloads

An Example of Generic Higher Order Functions in TypeScript, An example of how to write a generic higher order function in TypeScript using the built in type definitions ReturnType and Parameters. Ergonomic TypeScript Generics with Higher-Order Functions. Much of TypeScript ’s flexibility comes from its support for generics. They’re great for building up reusable abstractions so that you can share the “how” across your codebase even as the “what” varies significantly. In this post, I’ll describe a limitation that recently got in my way, and how I worked around it.

That some of them might return promises is a fact. That's the most TypeScript can know.

If they are or not all returning promises can only be determined at run time.

So the answer is no, TypeScript can't infer something that is only inferable at run time.

TypeScript 3.0 � TypeScript, When a function call includes a spread expression of a tuple type as the last an array type, and type inference can infer tuple types for such generic rest parameters. This enables higher-order capturing and spreading of partial parameter lists: T type T23<T> = T | unknown; // unknown // unknown in conditional types type� Statically typed functors, monads, and builders via higher order types in TypeScript. Giuseppe Maggiore. Follow. Aug 15, 2019

TypeScript 2.8 � TypeScript, TypeScript 2.8 introduces conditional types which add the ability to express from T to U (using the same inference algorithm as type inference for generic functions). For a given infer type variable V , if any candidates were inferred from can be nested to form a sequence of pattern matches that are evaluated in order: How to infer the types for objects and functions. TypeScript’s powerful inference helps us avoid bloating our code with lots of type annotations. TypeScript’s powerful inference helps us avoid bloating our code with lots of type annotations. The typeof keyword can help us when we want to strongly-type a variable from another variable’s type.

TypeScript generic higher order functions : typescript, TypeScript generic higher order functions. I was wondering if P : never; type NewFunction<T> = T extends (state: number, args: infer P) => any ? (args: P) Is it acceptable to use switch(true) to match the first met condition? I currently� Conditional Types # TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test: T extends U ? X : Y The type above means when T is assignable to U the type is X, otherwise the type is Y.

How to master advanced TypeScript patterns, Even famous libraries like Ramda do not provide generic types for their curry implementations Using function types, we were able to tell TypeScript to infer the tuple that we wanted. In this case, extends is referred to as a conditional type. type : We said that we needed tools in order to track arguments. Otherwise, the type inferred for V is never. Then, given a type T'' that is an instantiation of T where all infer type variables are replaced with the types inferred in the previous step, if T'' is definitely assignable to U, the conditional type is resolved to X.

Comments
  • I might have a solution for you but your code has no structural dependence of the T parameter, so I plan to exclude it.
  • The generic type is copy-pasted from the real code, this is just a simplification
  • Thank you, I'll give it a try
  • Makes sense. Could be a solution for you to split the invokeHandlers() in two functions, one that returns a boolean and the other returning a Promise<boolean>?
  • Yes, but then the one that can only return boolean will have to be typed in such a way that its parameters can never be promises, forcing you (as the programmer) to use the correct one from the start. And this would basically remove the advantage of being able to mix async and sync handlers. OR you could make the check at runtime instead of compile time, but then you loose the advantages of TypeScript.
  • Yes, that was the point. Currently I'm ignoring async handlers if the caller uses the sync version of the invokeHandlers(), but I was looking for a more clean solution