Search code examples
typescriptmapped-types

Typescript complex mapped types that extract generic


Similar to TypeScript Mapped Types: Get element type of array, I have a type, say:

type A = {
   Item1: Promise<string>,
   Item2: Promise<number>,
   Item3: number
}

I'd like to extract the following type from it:

type A' = {
   Item1: string,
   Item2: number,
   Item3: number
}

Note the added complexity of one of the fields NOT being a Promise.

Is this even possible or am I just reaching the limits of typescripts abilities to infer types? I tried fiddling around with mapped types and Record types but I just couldn't figure it out.

UPDATE:

Say I want to do the same with function calls instead:

type A = {
  Item1: (string, number) => Promise<string>,
  Item2: () => Promise<number>,
  Item3: () => number
}

And the desired type is:

type A' = {
  Item1: (string, number) => string,
  Item2: () => number,
  Item3: () => number
}

I thought this would be similar enough to the first case I state but function return values don't seem as straight forward as I would have hoped.


Solution

  • UPDATE 2020-01-28

    Since TypeScript 2.8 introduced conditional types you can now do this mapping with relative ease:

    type A = {
      Item1: (x: string, y: number) => Promise<string>,
      Item2: () => Promise<number>,
      Item3: () => number,
      Item4: Promise<string>,
      Item5: Promise<number>,
      Item6: number
    }
    
    type UnpromiseObj<T> = { [K in keyof T]: T[K] extends Promise<infer U> ? U :
      T[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R :
      T[K]
    }
    
    type Aprime = UnpromiseObj<A>;
    /* type Aprime = {
      Item1: (x: string, y: number) => string;
      Item2: () => number;
      Item3: () => number;
      Item4: string;
      Item5: number;
      Item6: number;
    } */
    

    I'll leave the below so that the crazy nonsense you had to go through before conditional types existed can be preserved for posterity:

    END UPDATE 2020-01-28


    It's not exactly impossible, but without official support for mapped conditional types or the like, it's iffy. Let's try. First, let's set up some type-level boolean logic:

    type False = '0';
    type True = '1';
    type Bool = False | True;
    type If<Cond extends Bool, Then, Else> = { '0': Else; '1': Then }[Cond];
    

    So the type If<True, Then, Else> evaluates to Then, and the type If<False, Then, Else> evaluates to Else.

    The first issue is that you need to be able to determine if a type is a Promise or not. The second is that you need to be able to get at the type of T, given a Promise<T>. I will do this by augmenting the declarations for the Object and Promise<T> interfaces with some phantom properties which won't exist at runtime:

    // if you are in a module you need to surround 
    // the following section with "declare global {}"
    interface Object {
      "**IsPromise**": False
    }
    interface Promise<T> {
      "**IsPromise**": True
      "**PromiseType**": T
    }
    

    That was the iffy part. It's not great to augment global interfaces, since they are in everyone's namespace. But it has the desired behavior: any Object which is not a Promise has a False type for its "**IsPromise**" property, and a Promise has a True value. Additionally, a Promise<T> has a "**PromiseType**" property of type T. Again, these properties don't exist at runtime, they're just there to help the compiler.

    Now we can define Unpromise which maps a Promise<T> to T and leaves any other type alone:

    type Unpromise<T extends any> = If<T['**IsPromise**'], T['**PromiseType**'], T>
    

    And MapUnpromise which maps Unpromise onto an object's properties:

    type MapUnpromise<T> = {
      [K in keyof T]: Unpromise<T[K]>
    }
    

    Let's see if it works:

    type A = {
       Item1: Promise<string>,
       Item2: Promise<number>,
       Item3: number
    }
        
    type Aprime = MapUnpromise<A>
    // evaluates to { Item1: string; Item2: number; Item3: number; }
    

    Success! But we've done some fairly unpleasant things to types to get it to happen, and it might not be worth it. That's up to you!

    Hope that helps; good luck!


    Update 1

    Doing the same with function calls is unfortunately not possible, as far as I can tell. You'd really need something like an extended typeof type query and that just isn't part of TypeScript for now (as of TypeScript v2.5 anyway).

    So you can't take your type A and compute APrime from it (note that A' isn't a valid identifier. Use if you want). But you can make a base type from which you can compute both A and APrime:

    type False = '0';
    type True = '1';
    type Bool = False | True;
    type If<Cond extends Bool, Then, Else> = { '0': Else; '1': Then }[Cond];
    type MaybePromise<Cond extends Bool, T> = If<Cond, Promise<T>, T>
    

    I've given up on global augmentation and added MaybePromise<Cond, T>, where MaybePromise<True, T> evaluates to Promise<T>, and MaybePromise<False, T> evaluates to T. Now we can get A and APrime using MaybePromise<>:

    type ABase<Cond extends Bool> = {
      Item1: (s: string, n: number) => MaybePromise<Cond, string>,
      Item2: () => MaybePromise<Cond, number>,
      Item3: () => number
    }
    
    type A = ABase<True>;
    // evaluates to { 
    //   Item1: (s: string, n: number) => Promise<string>; 
    //   Item2: () => Promise<number>; 
    //   Item3: () => number; }
    
    
    type APrime = ABase<False>;
    // evaluates to { 
    //   Item1: (s: string, n: number) => string; 
    //   Item2: () => number; 
    //   Item3: () => number; }
    

    So this works! But the refactoring I suggest might not play nicely with your use case. It depends on how you acquire the A type in the first place. Oh well, that's the best I can do. Hope it's of some help. Good luck again!