Allan Jardine
Allan Jardine

Reputation: 5463

Resolve a promise in a TypeScript type

This is a continuation of a question about resolving return types. I'm so close to what I'm looking to do (which is to have a common function to build model instances based on validated data) and it works fine in JS, but I can't figure out the final part for the TS typing.

The key is the return for my Init type - specifically the Builder<T[K]> part. I understand why that results in Builder<(id: number) => Promise<Test>> as the returned type, but I can't for the life of me figure out how to tell TS that in fact it is just Test being returned, not the function that returns a Promise of Test.

The following can be copy / pasted into VSCode (or whatever) to show the issue. The ter1 variable should be an instance of Test, and when compiled to JS it is, but TS doesn't see that.

export interface Builder<T> {
    (values: any, db: any): Promise<T>
}

export interface Wrapper {
    <T, TId>(
        construct: (id: TId, db: any | null) => Promise<T>,
        idSrc: string | string[],
        errorMsg: string
    ): Builder<T>
}

let builder: Wrapper = function (construct, idSrc, errorMsg ) {
    // A function that can be used to construct the instance
    return async function(values: any, db: any | null) {
        let id = null;
        let inst = await construct(id, db);

        if (!inst) {
            throw new Error(errorMsg);
        }

        return inst;
    };
}


class Test {
    is1 = true;

    static async fromId( id: number ) {
        var inst = new Test();
        // Some async action (e.g. a db read)
        return inst;
    }
}
class Test2 {
    is2 = true;

    static async fromId( id: number ) {
        var inst = new Test2();
        // Some async action (e.g. a db read)
        return inst;
    }
}


type Config<T extends {}> = {
    inputs?: {[index:string]: any},
    inst?: T;
};

type Init = <T extends {[key:string]: any}>(
    db: any,
    config: Config<T>
) => Promise<{[K in keyof T]: Builder<T[K]>}>; // ???

let init: Init = async function ( db, config ) {
    let ret: any = {};

    if ( config.inst ) {
        for (let [key, value] of Object.entries(config.inst)) {
            let res = await value( {}, {} );
            ret[ key ] = res;
        }
    }

    return ret;
}


async function allan () {
    let { ter1, ter2 } = await init( null, {
        inst: {
            ter1: Test.fromId,
            ter2: Test2.fromId
        }
    } );

    console.log( ter1.is1, ter1.is2 ); // should be `true undefined`

    // Test `builder` typing
    var t1Wrapper = builder( Test.fromId, 'id', 'test');
    var t2Wrapper = builder( Test2.fromId, 'id', 'test');

    var t1 = await t1Wrapper({}, {});
    var t2 = await t2Wrapper({}, {});

    t1.is1;
    t2.is2;
}

allan();

Thank you!

Upvotes: 6

Views: 8287

Answers (4)

0xcaff
0xcaff

Reputation: 13681

TypeScript 4.5 added the Awaited type.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#the-awaited-type-and-promise-improvements

// A = string
type A = Awaited<Promise<string>>;
// B = number
type B = Awaited<Promise<Promise<number>>>;
// C = boolean | number
type C = Awaited<boolean | Promise<number>>;

Upvotes: 6

Devin Rhode
Devin Rhode

Reputation: 25287

Just use AsyncReturnType from type-fest!

type-fest has something identical to @jcalz's Unpromise - it's simply named PromiseValue instead.

AsyncReturnType<FOO> is basically just PromiseValue<ReturnType<FOO>>

So, go ahead:

npm install type-fest --save-dev
yarn add type-fest --dev

And use like so:

type ServerData = AsyncReturnType<typeof getServerData>

Upvotes: 0

jcalz
jcalz

Reputation: 328252

It looks like the types you want are something like these:

type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never;

type Init = <T extends { [key: string]: (...args: any[]) => Promise<any> }>(
  db: any,
  config: Config<T>
) => Promise<{ [K in keyof T]: Unpromise<ReturnType<T[K]>> }>; 

But I haven't spent the time going through all your code to see if it makes sense or if you've implemented it correctly (is this really a minimum example?), so please forgive me if you run into some other problem.

Explanation: the way you call init() implies that T should be an object whose properties are functions that return a promise of the kind you care about. (I'm not sure if the types and numbers of arguments to those functions are important). So for each property in T you want to extract its return type and then extract the promise type.

Hope that helps. Good luck!

Upvotes: 15

kaosdev
kaosdev

Reputation: 309

The unit type is a function that return a Promise<{[K in keyof T]: Builder<T[K]>}>. Instead it should just return a Promise<{[K in keyof T]: T[K]}>;.

Also in the init function you are calling let res = await value( {}, {} ); But value is the fromId function of Test, that accept only one number parameter.

Upvotes: 0

Related Questions