1

I am attempting to map several Typescript types into one combined type via Typescript generic tuples. Example I would like to see:

// object 1
const foo = {
  user: {
    id: 1,
  },
};
// object 2
const bar = {
  hello: 'goodbye',
};

// later, after some code that combines the above objects into one
const baz = {
  user: {
    id: 1,
  },
  hello: 'goodbye',
};

I've gotten somewhere with generic tuples, and though the Typescript language server correctly autocompletes when I try creating an object, it errors when I try to use the mapped properties afterwards:

const foo = {
  user: {
    id: 1,
  },
};
const bar = {
  hello: "goodbye",
};

type Test<T extends Record<PropertyKey, any>[]> = (
  T[number] extends any ? (k: T[number]) => void : never
) extends (k: infer I) => void
  ? I
  : never;

const test: Test<[typeof foo, typeof bar]> = {
  user: {
    id: 1
  },
  hello: "23",
};

console.log(test.user);
                  ^
/* Property 'user' does not exist on type '{ user: { id: number; }; } | { hello: string; }'.
  Property 'user' does not exist on type '{ hello: string; }'.ts(2339) */

I am declaring a type that is mapped of all the generic tuples passed to it, it correctly autocompletes when I am creating a new object from the given types, but when I attempt to access those properties it errors!

Any reason this doesn't work, or am I not correctly typing this properly?

Jonathon Orsi
  • 137
  • 10
  • 1
    It would be better if your utility types were included in the post instead of the image of it. And a playground would be way much better. – Eldar Apr 24 '23 at 06:33
  • 1
    Like [this](https://tsplay.dev/mZZ7am) – Eldar Apr 24 '23 at 06:40
  • 1
    [Please replace images of code/logs/errors with plaintext versions.](https://meta.stackoverflow.com/a/285557/2887218) – jcalz Apr 24 '23 at 13:09

1 Answers1

1

Well, the problem is, your test type generates a union type. When you have a union type and want to access a property of it, you must convince the typescript compiler that it is safe to access that property. The way to do this is to create a type guard that ensures the property exists on the object. But from your question, it is understood that you don't want to have a union type, your object will have all the properties of the source object. So you need an intersection type. So a utility type (UnionToIntersection) given in this great so answer would help you.

Both, type guard and utility type in action :

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type TestUnion<T extends Record<PropertyKey, any>[]> = T[number];


type TestIntersection<T extends Record<PropertyKey, any>[]> = UnionToIntersection<T[number]>;

const test: TestUnion<[typeof foo, typeof bar]> = { user: { id: 3 }, hello: 'sd' };

const test2: TestIntersection<[typeof foo, typeof bar]> = { user: { id: 3 }, hello: 'sd' };

if (isFoo(test)) {
    if (test.user) { // is fine

    } else if (test.hello) { // not accessable

    }
}


if (test2.hello) {
    if (test2.user) { // both accessable

    }
}

function isFoo(val: any): val is typeof foo {
    return val.user

}

Playground

Edit

It might be better to build the intersection type directly, but it seemed to be way easier to use the already created utility type.

Eldar
  • 9,781
  • 2
  • 10
  • 35
  • 1
    Thanks! I actually just noticed that from the error right before coming back to this question. It definitely was an issue of having a union rather than an intersection type, and using you solution worked. – Jonathon Orsi Apr 24 '23 at 15:15