https://thadaw.com/posts/feed.xml

Nammatham Note 2: Convert array of keys and array of values to object

2023-01-25

This orignial question is from Stackoverflow. The full description how to construct this type utility (from catchts.com)

type A = ["cat", "dog"];
type B = ["red", "yellow"];

type Expected = {
  cat: "red",
  dog: "yellow"
}

The original solution

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

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 AllowedKeys<T> = T extends readonly Keys[] ? T : never;


type Mapper<T, U> = T extends Keys ? U extends Keys ? Record<T, U> : never : 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 A = ["cat", "dog"];
type B = ["red", "yellow"];
type Result = Zipper<A, B>;

const zip: Result = {
  cat: "red",
  dog: "yellow"
} // ok

playground

I want the Type support value any type, not only string | number | symbol as already defined in:

type Keys = string | number | symbol;
type AllowedKeys<T> = T extends readonly Keys[] ? T : never;

My Solution

Playground

To modify the original code, I've revise

type Keys = string | number | symbol;
type Mapper<T, U> = T extends Keys
  ? U extends Keys
    ? Record<T, U>
    : never
  : never;

โดยตัว Mapper ต้องการ value ของที่จะมา Map (U) เป็น Keys เท่านั้น ซึ่งก็คือ string | number | symbol อย่างใดอย่างหนึ่ง ซึ่งผมต้องการให้ Mapper รับ value ที่เป็นอะไรก็ได้

ดังนั้นจึงเหลือแค่เช็คว่า Key ที่รับเข้ามาเป็น Valid key หรือไม่ ซึ่งก็คือ string | number | symbol

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

จุดที่ 2 ที่แก้ไขก็คือ การนิยาย type Zip

type Keys = string | number | symbol;
type Zip<
  T extends ReadonlyArray<Keys>,
  U extends ReadonlyArray<Keys>,
  Result extends Record<string, any> = {}
> = // ...

โดยที่ Type รับ parameter 2 ตัวที่เป็น Readonly Array ที่เป็น Key ทั้ง key และ value เลยจึงทำให้ในการใช้งาน Zip

                    // v--- 2nd parameter needs to be `string | number | symbol`
Zip<["cat", "dog"], ["red", "yellow"]>;

จึงแก้ไขให้รับ any type แทน

Final Solution

type Length<T extends ReadonlyArray<any>> = T extends { length: infer L } ? L : never;
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 ?  Record<T, U>  : never;

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

/**
 * Recursive iteration through two arrays
 */
type Zip<
  T extends ReadonlyArray<Keys>,
  U extends ReadonlyArray<any>,
  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>, 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<any>> = CompareLength<T, U> extends true
  ? Zip<T, U>
  : never;

class Red {}
class Yellow {}

type A = ["cat", "dog"];
type B = [Red, Yellow];
type Result = Zipper<A, B>;

const zip: Result = {
  cat: Red,
  dog: Yellow
} // ok
powered by zola and serene