aleclofabbro
aleclofabbro

Reputation: 1699

Complex signature in Typescript function

Following generic function takes a object of functions and returns an object with same propnames valued with returning value of the corresponding function.

const combineValues = obj => { 
      const res = {};
      for (const k in obj){
        res[k] = obj[k]()
      }
      return res;
    } 
const combined = combineValues({
  n: () => 1,
  s: () => 's'
}); // { n: 1, s: 's' }

Tryed to define a signature and an implementation for this function in Typescript

const combineValues =
  <K extends string> (obj: Record<K, () => any>): Record < K, any > => {
    const res = {} as Record<K, any>;
    for (const k in obj){
      res[k] = obj[k]()
    }
    return res;
  }

const combined = combineValues({
  n, s
}) // Record<"n" | "s", any>

but combined doesn't keep original typings for values

const combcombineValues = <K extends string, T>(obj: Record<K, () => T>): Record<K, T> => {
    const res = {} as Record<K, T>;
    for (const k in obj){
      res[k] = obj[k]()
    }
    return res;
  }

works only if all function props return same type T

is it possible to fully define it in Typescript ?

Upvotes: 2

Views: 730

Answers (2)

jcalz
jcalz

Reputation: 327634

This can be done in the current version of TypeScript (v2.7) without waiting for conditional types, by using inference from mapped types, a feature of TypeScript in which functions can infer types in what seems like the "backwards" direction, from output to input.

First let's define the mapped type Funcs<T> which takes a plain object and turns it into an object whose properties are all functions returning the property types of the plain object:

type Funcs<T> = { [K in keyof T]: (...args:any[]) => T[K] }

Note how this type is basically backwards from what you want to do. Now let's type combineValues():

const combineValues = <T>(obj: Funcs<T>): T => {
    const res = {} as T;  // only change in body
    for (const k in obj) {
        res[k] = obj[k]()
    }
    return res;
}

which, as you can see, takes a Funcs<T> as input and returns a T as output. Let's see if it works:

const combined = combineValues({
    n: () => 1,
    s: () => 's'
}); // { n: number, s: string }

That's almost what you wanted. The only difference is that TypeScript tends to interpret () => 1 as a function that returns a number and not a function that returns the literal 1. There are things you can do to work around that; the simplest (although a bit repetitive) is to assert the return type literals:

const combined = combineValues({
    n: () => 1 as 1,
    s: () => 's' as 's'
}); // { n: 1, s: 's' }

Hope that helps. Good luck!

UPDATE: It was noted that the compiler appears to type combined as {n: any, s: any} in TypeScript 2.7. Fortunately, this only seems to be happening when you inspect the type with Intellisense, as (I think) noted in Microsoft/TypeScript#14041. If you actually use the value, you will see that it has the proper type:

combined.n = 0 // error, 0 is not assignable to 1
combined.s = 0 // error, 0 is not assignable to 's'

Playground link

So, I guess I stand by this answer, although the Intellisense bug is unfortunate. Good luck again!

Upvotes: 3

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249476

You can achieve this in typescript 2.8 using conditional types (2.8 in not yet released at the time of writing, you can get it via npm install -g typescript@next it is planned for a March release).

// ReturnType<T> below is from 2.8 and is a conditional type
type AllReturnTypes<T extends { [name: string]: (...args: any[]) => any }> = { [P in keyof T]: ReturnType<T[P]> }
const combineValues = <T extends { [name: string]: () => any }>(obj: T): AllReturnTypes<T> => {
    const res = {} as AllReturnTypes<T>;
    for (const k in obj) {
        res[k] = obj[k]()
    }
    return res;
}

const combined = combineValues({
    n: () => 1, s: () => ""
}) // will be of type { n: number, s: string }

Upvotes: 2

Related Questions