17

I'm trying to make a type-safe map function (not the function below), but I'm stuck on getting the function parameters to infer correctly.

    export type Mapper<U extends Unmapped> = {
      mapped: Mapped<U>
    };

    export type Unmapped = {
      [name: string]: (...args: any[]) => any
    };

    export type Mapped<U extends Unmapped> = {
      [N in keyof U]: (...args: any[]) => Promise<any>
    };

    const map = <U extends Unmapped>(unmapped: U): Mapper<U> => ({
      mapped: Object.entries(unmapped).reduce(
        (previous, [key, value]) => ({
          ...previous,
          [key]: (...args: any[]) => new Promise((resolve) => resolve(value(...args)))
        }),
        {}
      ) as Mapped<U>
    });

    const mapped = map({ test: (test: number) => test });

    mapped.mapped.test('oh no');

Is it possible to let TypeScript infer them? Currently the functions inside the mapped object accept any parameters, but it should only take the parameters defined in the unmapped object. The function names do get inferred correctly.

2 Answers 2

33

Can use Parameters and ReturnType generic types to get the specific parameters and return type of the function:

type Promisified<T extends (...args: any[]) => any> = (...args: Parameters<T>) => Promise<ReturnType<T>>;

export type Mapped<U extends Unmapped> = {
    [N in keyof U]: Promisified<U[N]>
}
Sign up to request clarification or add additional context in comments.

Comments

9

If you use (...args: any[]) => Promise<any> as the signature in the mapped type you will loose all parameter type info and return type info. An imperfect solution to what you want to do can be achieved using conditional types. The limitations are described here.

The solution would require the creation of a conditional type that handles each function with a given number of parameters separately. The solution below works for up to 10 parameters (more then enough for most practical cases)

export type Mapper<U extends Unmapped> = {
    mapped: Mapped<U>
};

export type Unmapped = {
    [name: string]: (...args: any[]) => any
};

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;

type Promisified<T extends Function> =
    T extends (...args: any[]) => Promise<any> ? T : (
        T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => infer R ? (
            IsValidArg<J> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => Promise<R> :
            IsValidArg<I> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => Promise<R> :
            IsValidArg<H> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => Promise<R> :
            IsValidArg<G> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => Promise<R> :
            IsValidArg<F> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F) => Promise<R> :
            IsValidArg<E> extends true ? (a: A, b: B, c: C, d: D, e: E) => Promise<R> :
            IsValidArg<D> extends true ? (a: A, b: B, c: C, d: D) => Promise<R> :
            IsValidArg<C> extends true ? (a: A, b: B, c: C) => Promise<R> :
            IsValidArg<B> extends true ? (a: A, b: B) => Promise<R> :
            IsValidArg<A> extends true ? (a: A) => Promise<R> :
            () => Promise<R>
        ) : never
    );

export type Mapped<U extends Unmapped> = {
    [N in keyof U]: Promisified<U[N]>
}

const map = <U extends Unmapped>(unmapped: U): Mapper<U> => ({
    mapped: Object.entries(unmapped).reduce(
        (previous, [key, value]) => ({
            ...previous,
            [key]: (...args: any[]) => new Promise((resolve) => resolve(value(...args)))
        }),
        {}
    ) as Mapped<U>
});

const mapped = map({ test: (test: number) => test });

mapped.mapped.test('oh no'); 

Comments

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.