Reputation: 1699
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
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'
So, I guess I stand by this answer, although the Intellisense bug is unfortunate. Good luck again!
Upvotes: 3
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