[FIXED] How to extract object values and convert to spread/rest arguments?

Issue

I want to generate TypeScript definitions for JavaScript code in Magento 2 or RequireJS to be more precise.

Example:

// Generated
const modules = {
  a: 1,
  b: 2,
  c: 3,
  test: { nesttest: 'Nice' }
} as const;

// Module
define(['test', 'a', 'b', 'unknown'], function (test, a, b, UnKnown) {
  // test = { nesttest: 'Nice' }
  // a = 1
  // b = 2
  // UnKnown = any | unknown
});

This is what I have so far. It is only for TypeScript, but I know how to continue after this problem:

  • The trueCallback should return the same values as the pseudoCallback
  //---------------//
 //--- Modules ---//
//---------------//

const modules = {
  a: 1,
  b: 2,
  c: 3,
  test: { nestedtest: 'Nice' }
} as const



  //-------------------//
 //--- Definitions ---//
//-------------------//

function define<
  Mods extends typeof modules,
  Keys extends keyof Mods,
  Input extends ReadonlyArray<InputVal>,
  InputVal extends string,
  InputUnion extends Input[number],
  InputArr extends TuplifyUnion<InputUnion>,
  Values extends Mods[Keys],
  ValuesArr extends TuplifyUnion<Values>,
>(
  keys: Readonly<Input>,
  pseudoCallback: (...args: [Mods['test'], Mods['b'], Mods['a']]) => void,
  trueCallback: (...args: unknown[]) => void, // WHAT COMES HERE?
) {
  const obj = modules as Mods;
  const values = keys.map(key => {
    if (key in obj) {
      return obj[key as keyof Mods];
    }

    return undefined;
  });

  trueCallback(...values);
}



  //-------------//
 //--- Usage ---//
//-------------//

define(['test', 'b', 'a'], function (test, b, a) {
  test
  // ^?

  const nested = test.nestedtest
  nested
  // ^?

  b
  // ^?

  a
  // ^?

  // SHOULD BE LIKE THIS
}, function (test, b, a) {
  test
  // ^?

  b
  // ^?

  a
  // ^?

  // DOES NOT WORK
})



  //-------------//
 //--- Types ---//
//-------------//

// Converts Union To Array
type TuplifyUnion<
  T,
  L = LastOf<T>,
  N = [T] extends [never] ? true : false
> = true extends N
  ? []
  : Push<TuplifyUnion<Exclude<T, L>>, L>;

type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never;

type Push<T extends any[], V> = [...T, V];

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

type ObjValueTuple<
  T extends any,
  KS extends Array<any> = TuplifyUnion<keyof T>,
  R extends Array<any> = []
> = KS extends [infer K, ...infer KT]
  ? ObjValueTuple<T, KT, [...R, T[K & keyof T]]>
  : R;

type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
}

type DeepMutable<T> = {
  -readonly [K in keyof T]: T[K] extends object ? DeepMutable<T[K]> : T[K];
}

TypeScript Playground

Solution

For simplicity, I created a type Mods that just holds typeof modules.

const modules = {
  a: 1,
  b: 2,
  c: 3,
  test: { nestedtest: 'Nice' }
} as const

type Mods = typeof modules

Let’s just use a single generic type K to represent the keys as a tuple type.

function define<
  K extends (keyof Mods)[]
>(
  keys: [...K],
  /* ... */
) {
  /* ... */
}

For trueCallback we can map over the tuple K with a mapped type. For each index I we take K[I] and use it to index Mods.

function define<
  K extends (keyof Mods)[]
>(
  keys: [...K],
  trueCallback: (...args: { 
    [I in keyof K]: Mods[K[I] & keyof Mods] 
  }) => void
) {
  /* ... */
}

For the implementation of the function, we can use an any assertion when we call trueCallback. TypeScript can’t really understand that values has the correct type to call trueCallback.


This leads to the following end result:

function define<
  K extends (keyof Mods)[]
>(
  keys: [...K],
  trueCallback: (...args: { 
    [I in keyof K]: Mods[K[I] & keyof Mods] 
  }) => void
) {
  const obj = modules as Mods;
  const values = keys.map(key => {
    if (key in obj) {
      return obj[key as keyof Mods];
    }

    return undefined;
  });

  trueCallback(...values as any);
}

define(['test', 'b', 'a'], function (test, b, a) {
  test 
  // ^?{ readonly nestedtest: "Nice"; }

  b
  // ^? 2

  a
  // ^? 1
})

Playground

Answered By – Tobias S.

Answer Checked By – Marilyn (Easybugfix Volunteer)

Leave a Reply

(*) Required, Your email will not be published