hopeless-programmer
hopeless-programmer

Reputation: 990

Merge value types of tuple of objects in TypeScript

I have a set of objects, each with its own properties:

const a = { a1 : 1, a2 : 2 } as const
const b = { b1 : `1`, b2 : `2` } as const

The function f takes all these objects as a typed tuple:

function f<
    T extends { [key : string] : any }[]
> (
    ...t : [...{ [i in keyof T] : T[i] }]
) {
    // todo
}

f(a, b)

The goal is to return any property of any of these objects. In this case, the expected result should be 1 | 2 | "1" | "2".

The problem is that I can't figure out how to properly describe the return type.

I have tried T[number][keyof T[number]] but it have failed, probably due to possible differences in indexes for T and keyof T.

Then I wrote a wrapper for it:

type PropertyOf<T extends { [key : string] : any }> = T[keyof T]

And specify the return type of f as PropertyOf<T[number]>. But it still doesn't work.

Despite PropertyOf returns expected 1 | 2 for PropertyOf<{ a1 : 1, a2 : 2 }>, when used as PropertyOf<T[number]> in f the function return type is never.

What is the reason and how to fix this? Thanks.

Upvotes: 2

Views: 139

Answers (2)

jcalz
jcalz

Reputation: 327944

You could use this approach:

declare function f<T extends object[]>(
    ...t: T
): { [I in keyof T]: T[I][keyof T[I]] }[number]
  • note that object is often more forgiving for callers than { [key : string] : any } because the latter's index signature prevents you from passing in values of interface types; see ms/TS#15300.

  • note that your input type [...{ [i in keyof T] : T[i] }] is indistinguishable from T for a rest parameter.

But importantly, we are mapping over the T tuple type and computing your PropertyOf<T> type (also called ValueOf<T> according to Is there a `valueof` similar to `keyof` in TypeScript? ) for each tuple element, then indexing into the resulting tuple with number, resulting in a union of all the types in that tuple.

Let's test it out:

const a = { a1: 1, a2: 2 } as const
const b = { b1: `1`, b2: `2` } as const

const ret = f(a, b);
// const ret: 1 | 2 | "1" | "2"

Looks good.

Playground link to code

Upvotes: 2

Tobias S.
Tobias S.

Reputation: 23825

We can use the following as the return type for f.

function f<
    T extends { [key : string] : any }[]
> (...t : [...{ [i in keyof T] : T[i] }])
  : T[number] extends Record<string, infer U> ? U : never 
{
   return {} as any
}

const result = f(a, b)
//    ^? const result: 1 | 2 | "1" | "2"

Playground


The keyof operator does not work here because it only returns shared properties when used on object unions. Both objects don't share any properties, so keyof evaluates to never.

Upvotes: 1

Related Questions