Tariq Qubti
Tariq Qubti

Reputation: 43

Typescript functions return type is not captured

What am I missing here, result is of type {foo: unknown, bar: unknown, baz: unknown} I think it should be of type {foo: number, bar: boolean, baz: string}

export function apply<A, B extends {[P in keyof A]: B[P]}>(
  functions: {[P in keyof A]: (a: A[P]) => B[P]},
  data: {[P in keyof A]: A[P]},
): {[P in keyof A]: B[P]} {
  const result = {} as {[P in keyof A]: B[P]}
  const keys = Object.keys(functions) as (keyof A)[]
  for(const key of keys)
    result[key] = functions[key](data[key])
  return result
}

const functions = {
  foo: (a: string) => a.length,
  bar: (a: number) => a === 42,
  baz: (a: boolean) => a ? 'true' : 'false'
}
const data = {foo: 'foo', bar: 42, baz: true}
const result = apply(functions, data)

Upvotes: 0

Views: 32

Answers (1)

CRice
CRice

Reputation: 32176

First let's examine the issues with the current attempt, namely the types you've given to the two arguments.

functions: {[P in keyof A]: (a: A[P]) => B[P]},
data: {[P in keyof A]: A[P]}

Looks like you want the type A[P] to represent the value types of the A object. The problem here is that there are no restrictions on the type A, so there is nothing we can use to infer a more specific type for the values. Thus they are inferred to have the most general type: unknown.

Consider that you can manually specify generic parameters when you invoke the apply function. Something like:

type A = {} // ??? what to put here?
type B = {} // ??? what to put here?
const result = apply<A, B>(functions, data)

The problem is I am not sure what I would give as the A type in this example. Indeed I'm not sure that a valid type for A even exists in this construction, other than what you've seen: {foo: unknown, bar: unknown, baz: unknown}.


So instead of using a mapped type for the arguments like above, why not add these restrictions to the generic signature itself? It looks like you want the functions parameter to be an object holding single argument functions. Let's restrict the generic A to meet that condition right away:

A extends Record<string, (a: any) => any>

And for B, this is the type of your data, it should match to the argument type of each corresponding function in A. We can also apply that restriction within the generic signature:

B extends {[P in keyof A]: Parameters<A[P]>[0]}

And finally, for the return type, this is another object but instead of corresponding to the argument type of A, it is the return type:

{[P in keyof A]: ReturnType<A[P]>

Putting all that together:

function apply<A extends Record<string, (a: any) => any>, B extends {[P in keyof A]: Parameters<A[P]>[0]}>(
    functions: A,
    data: B
): {[P in keyof A]: ReturnType<A[P]> {
    const result = {} as {[P in keyof A]: B[P]}
    const keys = Object.keys(functions) as (keyof A)[]
    for(const key of keys)
        result[key] = functions[key](data[key])
    return result
}

const functions = {
    foo: (a: string) => a.length,
    bar: (a: number) => a === 42,
    baz: (a: boolean) => a ? 'true' : 'false'
}
const data = {foo: 'foo', bar: 42, baz: true}

const result = apply(functions, data)
// The type of `result` is inferred to be:
// {
//    foo: number;
//    bar: boolean;
//    baz: "true" | "false";
// }

Upvotes: 1

Related Questions