Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inferring function parameters in TypeScript

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.

like image 897
houfio Avatar asked Jun 09 '18 09:06

houfio


People also ask

How do you define a function parameter in TypeScript?

In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature: function firstElement < Type >( arr : Type []): Type | undefined {

Can TypeScript infer types?

TypeScript infers types of variables when there is no explicit information available in the form of type annotations. Types are inferred by TypeScript compiler when: Variables are initialized. Default values are set for parameters.

Should I let TypeScript infer types?

Letting TS infer will give you more freedom when refactoring types and it makes your code more flexible. This is the main reason why most of the time I recommend against annotating loose variables or callback/promise arguments.

How do I declare optional parameters in TypeScript?

In Typescript, making optional parameters is done by appending the “?” at the end of the parameter name in the function when declaring the parameters and the parameters which are not marked with “?” i.e not optional parameter are called as default parameters or normal parameters where it is must and compulsory to pass ...


2 Answers

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'); 
like image 144
Titian Cernicova-Dragomir Avatar answered Oct 30 '22 15:10

Titian Cernicova-Dragomir


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]>
}
like image 24
Tadhg McDonald-Jensen Avatar answered Oct 30 '22 14:10

Tadhg McDonald-Jensen