1

I'm trying to create a type that takes two lists as an input:

Keys: ["foo", "bar", "baz"]
Values: [50, number, "abc"]

to, then, output the following object composed from their keys and values:

{
    foo: 50,
    bar: number,
    baz: "abc"
}
type UnionToIntersection<T> = (T extends object ? (k: T) => void : never) extends (k: infer U) => void ? U : never;

type KeyValuePairsFromLists<
    Keys extends Array<string | number>,
    Values extends Array<any>
> = {
    [index in keyof Keys]: index extends keyof Values
        ? [Keys[index], Values[index]]
        : never;
};
type ObjectFromKeyValuePairs<
    KV extends Array<[string | number, any]>,
    T = {
        [index in keyof KV]: KV[index] extends [string | number, any]
            ? Record<KV[index][0], KV[index][1]>
            : never;
    }
> = UnionToIntersection<T[keyof T]>;

type ObjectFromKeyValueArrays<
    Keys extends Array<string | number>,
    Values extends Array<any>
> = ObjectFromKeyValuePairs<KeyValuePairsFromLists<Keys, Values>>;
const myObj = {} as ObjectFromKeyValueArrays<
    ["foo", "bar"],
    [5,     "bar"]
>;

type foo = typeof myObj.foo // 5
type bar = typeof myObj.bar // string

The way I'm doing it now is by converting the arrays into key/value pairs, then mapping them to a union of objects with a single key, and converting that to an intersection to get the final object.

Is there some better way of achieving this? Am I doing this wrong?

4
  • Your example does not compile. See this line ObjectFromKeyValuePairs<KeyValuePairsFromLists<Keys, Values>> Commented Apr 9, 2021 at 13:12
  • Just a thought (although I can't solve the whole thing) using type ObjectFromKeyValueArrays<Keys extends string[], Values extends any[] & Pick<Keys,"length">> gave me nice coupling between length of Key array and Value array which could eliminate some cases. Commented Apr 9, 2021 at 13:20
  • @captain-yossarian I edited it. Should work now. Commented Apr 9, 2021 at 13:20
  • Thanks for accepting. I made a small update about type casting Commented Apr 9, 2021 at 13:45

1 Answer 1

1

Solution:


type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
  ? L
  : never;

// I believe we should check first if length of arrays is equal:

type CompareLength<
  X extends ReadonlyArray<any>,
  Y extends ReadonlyArray<any>
  > = Length<X> extends Length<Y> ? true : false;

/**
 * Let's operate on primitives 
 */
type Keys = string | number | symbol;

type Mapper<T, U> = T extends Keys ? U extends Keys ? Record<T, U> : never : never;

type AllowedKeys<T> = T extends readonly Keys[] ? T : never;

/**
 * Recursive iteration through two arrays
 */
type Zip<T extends ReadonlyArray<Keys>, U extends ReadonlyArray<Keys>, Result extends Record<string, any> = {}> =
  CompareLength<T, U> extends true
  ? T extends []
  ? Result :
  T extends [infer HeadT1]
  ? U extends [infer HeadU1]
  ? Result & Mapper<HeadT1, HeadU1> : never :
  T extends [infer HeadT2, ...infer TailT2]
  ? U extends [infer HeadU2, ...infer TailU2]
  ? Zip<AllowedKeys<TailT2>, AllowedKeys<TailU2>, Result & Mapper<HeadT2, HeadU2>>
  : never
  : never
  : never;

/**
 * Apply Zip only if arrays length is equal, otherwise return never
 */
type Zipper<T extends ReadonlyArray<Keys>, U extends ReadonlyArray<Keys>> =
  CompareLength<T, U> extends true ? Zip<T, U> : never;

type Z = Zipper<["foo", "bar"], [5, "bar"]>;

type Test = Zipper<["foo", "bar"], [5, "bar", 2]>; // never, length is not equal

// Record<"foo", 5> & Record<"bar", "bar">
const zip: Z = {
  foo: 5,
  bar: 'bar'
} // ok

Playground

You can find more information about generic iteration through tuples in my blog

Am I doing this wrong?

I would say that your approach is ok but a little bit unsafe because it does not compile and you are forced to use as operator (type casting):

/**
 * Record<"foo", 5> & Record<"bar", "bar"> 
 * & (() => string) & (() => string) 
 * & (() => Record<"foo", 5> | Record<"bar", "bar"> | undefined) 
 * & ((...items: (Record<"foo", 5> | Record<...>)[]) => number) 
 * & ... 24 more ... & ((searchElement: Record<...> | Record<...>, fromIndex?: number | undefined) => boolean)
 */
type Check = ObjectFromKeyValueArrays<
    ["foo", "bar"],
    [5,     "bar"]
>; 

// Does not compile
const myObj:ObjectFromKeyValueArrays<
    ["foo", "bar"],
    [5,     "bar"]
> = {
  foo:5,
  bar:'bar'
} 

Please keep in mind, if you are using type casting, it is 50% that you did smth wrong.

Sign up to request clarification or add additional context in comments.

4 Comments

I don't think advising against type casting (with made up percentages) is a good advise. Sometimes, there are things you can only do with type casting and this type of advise could push beginners away from an otherwise perfectly valid tool in their toolbox.
Ok, I made it 50% :)
Btw, I don't think @tacheometry is beginner
He's probably not, but answers to the questions asked here are not only for the people asking them. They also get indexed by search engines and serve as references for other people looking for solutions to different problems.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.