#How can I map the types in a tuple to the types in an object?

42 messages · Page 1 of 1 (latest)

stray knot
#

There's a repetitive bit of code that I find myself writing constantly, looks like this:

@Component({/* ... */})
public class SomeComponent {
  private stateA$: Observable<TypeA> = // ... some RXJS
  private stateB$: Observable<TypeB> = // ... some RXJS
  private stateC$: Observable<TypeC> = // ... some RXJS

  // Repetative code here
  public componentState$ = combineLatest([
    stateA$,
    stateB$,
    stateC$,
  ]).pipe(
    map(([foo, bar, baz]) => ({
      foo,
      bar,
      baz,
    })
  )
}

If it's not obvious, the reason for this is because it simplifies writing the component (*ngIf="componentState$ | async as state" on the parentmost div), and accessing state.bar seems a lot safer than state[1].

Since I write this so frequently, I've been trying to wrap it in a generic operator, but I can't seem to get the generics working so that the types are preserved. Here's the toObject() function:

function toObject<T extends string, U>(keys: T[], values: ReadonlyArray<U>) {
  return keys.reduce(
    (obj, key, index) => ({ ...obj, [key]: values[index] }),
    {} as { [key in T]: typeof values[number] },
  );
}
const test = toObjectHelper(['a', 'b', 'c'], [1, true, 'hello']);

Of course, the type of test not right.

// What my heart wants it to be:
{
  a: number,
  b: boolean,
  c: string,
}
// What it is:
{
  a: string | number | boolean;
  b: string | number | boolean;
  c: string | number | boolean;
}

I feel like what I need is something like:

// first key is typeof V[0], second key is typeof V[1], etc.
function toObjectHelper(/* ... */): { [ key in T]: U[indexOf key] } {
  // ...
}

I've seen some solutions in the RXJS source (like the source for pipe()) where there is essentially an overload for any number of inputs, up to a maximum amount. Is that what I'd have to here, or is there a cleaner way?

Thanks

stray knot
#

Dang this looks like it does what I'm looking for... Sorry, took off from work yesterday and didn't see this message util just now @daring cargo

#

How the heck does that work? I can't wrap my head around it, the infers are throwing me for a loop

#

And the spread syntax, haven't seen that used with type aliases

plush jasper
#

@stray knot

K extends [infer KH, ...infer KR]

basically pulls the first entry off the list.

#

This is a fairly common recursive pattern where you handle a list of items by pulling the first item off, doing something with it, and then recursively call the function with the remaining items.

stray knot
#

Ahhhhhh

#

Holy cow

#

That's incredible

plush jasper
#

Like here's JS code that uses a similar pattern.

function dotProduct(arr1: number[], arr2: number[]) {
  if(arr1.length === 0 || arr2.length === 0) return 0;
  const [head1, ...rest1] = arr1;
  const [head2, ...rest2] = arr2;
  return head1 * head2 + dotProduct(rest1, rest2);
}
stray knot
#

So KH is the type of the first element in the K tuple, VH is the type of the first element in the V tuple

Record<KH & PropertyKey, VH> would be the the first key/value type, then it intersects that with the recursive call on the rest of the tuple members (KR, VR)

#

Guessing the H in KH and VH is head, the R is rest

plush jasper
#

Yup.

stray knot
#

Good lord

plush jasper
#

Type definitions are basically functions in the functional programming sense (produce 'values', can't have side-effects), it's fairly common that complex type definitions are often written somewhat like functional programming.

stray knot
#

Okay, going to try to adapt that to take (['keyOne', 'keyTwo', 'keyThree'], [objectOne, objectTwo, objectThree]) and return something to the effect of { keyOne: typeof objectOne, keytwo: typeof objectTwo, keyThree: typeof objectThree }, think I can see how that would be done, just need to play with it

#

Actually I'm not totally sure that I even need to do that, as it is, the resulting type is just a stricter version of what I was originally imagining the alias would do

#

I think.

#

So on V extends [infer VH, ...infer VR] I understand that VH is the inferred type of V[0]. And what I'm seeing in this example is that VH is the narrowest possible type of V[0], which is the type containing only the actual value of V[0]. Is that correct, and is that how infer always works?

type Result = ToObject<['foo', 'bar', 'baz'], ['Fun', true, { key: 'value'}]>

What I mean is that the first member of the intersection is Record<"foo", "Fun"> instead of Record<"foo", string>, which is surprising to me at first but I can see how that should work fine for my needs

plush jasper
#

Yeah, ['Fun', true, { key: 'value'}] is already a type (this is all part of a type expression) so there isn't any "converstion from runtime values to types" going on here.

stray knot
#

One other thing that's not obvious to me is what the intersection with PropertyKey in the first argument to Record<> is doing, is that just a safety thing?

#

Oh actually now I see that removing it produces an error

#

Because record wants something that extends string | number | symbol

plush jasper
#

Yeah, there's currently nothing that prevents KH from being something that isn't a valid key.

stray knot
#

Gotcha

plush jasper
#

I thought that K extends PropertyKey[] might do it, but apparently not. (Though it's not a bad idea)

stray knot
#

Oh yeah was just thinking the same thing

plush jasper
#

Nowadays, you can also put constraints on extends itself, so this could also be:

type ToObject<K extends PropertyKey[], V extends unknown[]> =
    K extends [infer KH extends PropertyKey, ...infer KR extends PropertyKey[]]
        ? V extends [infer VH, ...infer VR]
            ? Record<KH, VH> & ToObject<KR, VR>
            : {}
        : {}
#

Though this is a bit more repetitive, it is kinda nice that you get an obvious compile error if you try to pass a non-valid key into the array.

stray knot
#

Ah I was almost there haha, still not totally sure of the rules arounds infer

stray knot
#

Okay I think that this all makes sense, really appreciate the help @plush jasper and @daring cargo

#

Damn

type ToObject<K extends PropertyKey[], V extends unknown[]> =
  K extends [infer KH extends PropertyKey, ...infer KR extends PropertyKey[]]
      ? V extends [infer VH, ...infer VR]
          ? Record<KH, VH> & ToObject<KR, VR>
          : {}
      : {}

function toObjectHelper<K extends PropertyKey[], V extends unknown[]>(
  keys: K,
  values: V,
): ToObject<K, V> {
  return keys.reduce(
    (obj, key, index) => ({ ...obj, [key]: values[index]}), {} as ToObject<K, V>,
  );
}

const test = toObjectHelper(['a', 'b', 'c'], [1, true, 'hello']);
proven sirenBOT
#
beans_1305#0

Preview:```ts
type ToObject<
K extends PropertyKey[],
V extends unknown[]

= K extends [
infer KH extends PropertyKey,
...infer KR extends PropertyKey[]
]
? V extends [infer VH, ...infer VR]
? Record<KH, VH> & ToObject<KR, VR>
: {}
: {}

function toObjectHelper<
K extends PropertyKey[],
V extends unknown[]

(keys: K, valu
...```

stray knot
#

!open

#

Ah, if I change the call to const test = toObjectHelper(['a', 'b', 'c'] as const, [1, true, 'hello'] as const);, the typing seems to work correctly

jaunty garnet
#

you can also use const type parameters to avoid the need for as const at the call site

#

i'd slap some readonlys in there too to make it more general

#

like so: