5

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.

2 Answers 2

9

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!

Sign up to request clarification or add additional context in comments.

6 Comments

This is great! I've never really looked at writing logic with types before. Is there anywhere specific I can learn more about this kind of logic? I'd like to be able to recognize these kinds of issues. Also, is there a reason why type False is not just = false instead of the string value 0?
Ah, I see. Can't use booleans as keys into an object for the If type
So turns out my toy case didn't exactly fit my real case scenario, I forgot that the promises were actually wrapped in a function. I've updated the question.
TypeScript doesn't give you any great handle to take a function type and get its return type. I could suggest a refactoring that is simpler but might not be how you want to go. I'll think about it and update the answer.
If you're interested in the idea of encoding logic and other ideas into your type system, look up "type-level programming". Languages like Scala and Haskell make use of this. TypeScript is so new that I don't see a lot of resources for type-level programming in it.
|
1

It has gotten easier nowadays with TypeScript 4.5's Awaited utility type:

type A = {
  Item1: Promise<string>;
  Item2: Promise<number>;
  Item3: number;
  Item4: (arg0: string, arg1: number) => Promise<string>;
  Item5: () => Promise<number>;
  Item6: () => number;
};

type AwaitValues<T> = {
  [K in keyof T]: T[K] extends (...args: infer P) => infer R
    ? (...args: P) => Awaited<R>
    : Awaited<T[K]>;
};

type A2 = AwaitValues<A>;

And by hovering A2 we can see its type to be:

/*
type A2 = {
    Item1: string;
    Item2: number;
    Item3: number;
    Item4: (arg0: string, arg1: number) => string;
    Item5: () => number;
    Item6: () => number;
}
*/

2 Comments

You can do this even more succinctly by inferring the parameters and return type! tsplay.dev/NrKjzm
Nice! Let me edit that in.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.