6

I have array where each item is array [name: string, someFunction: Function]. I would like to convert it to object, where keys are names and values are someFunctions:

// Input
const arrayFunctions = [
    ['getLength', (text: string) => text.length],
    ['setValue', (id: string, value: number) => {}],
    ['getAll', () => ([1, 2, 3])]
]

// Output
const objectFunctions = {
    getLength: (text: string) => text.length,
    setValue: (id: string, value: number) => {},
    getAll: () => ([1, 2, 3])
}

Is there any way to connect type of function in input array and type of function in output object?

type ObjectFunctions<ArrayFunctions> = { [/* Value from ArrayFunctions[i][0] */]: /* Value from ArrayFunctions[i][1] */ }

const arrayToObject = <ArrayFunctions extends Array<any>>(functions: ArrayFunctions) => {
    const result = {}

    for (const [name, func] of functions) {
        result[name] = func
    }

    return result as ObjectFunctions<ArrayFunctions>
}

const arrayFunctions = [
    ['getLength', (text: string) => text.length],
    ['setValue', (id: string, value: number) => {}],
    ['getAll', () => ([1, 2, 3])]
]

const objectFunctions = arrayToObject(arrayFunctions)

const length = objectFunctions.getLength() // Should be error because first parameter (text) is missing.
objectFunctions.setValue(true, 2) // Should be error, because of first parameter (id) must be string.
Michal
  • 1,755
  • 3
  • 21
  • 53

3 Answers3

9

It is possible if the array is defined at compile time, so typescript will have a chance to infer the types.

Convert inner tuple to object:

type ToObject<T> = T extends readonly [infer Key, infer Func]
  ? Key extends PropertyKey
  ? { [P in Key]: Func } : never : never;

This will allow us to convert ['getLength', (text: string) => text.length]
To { getLength: (text: string) => number }

Convert array of tuples to array of objects (mapped type on array):

type ToObjectsArray<T> = {
  [I in keyof T]: ToObject<T[I]>
};

This will allow us to convert array of arrays to array of objects.
We can now extract union of desired objects by querying the type of array item Array[number].

The last step - we actually need intersection instead of union. We can use the famous UnionToIntersection:

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

Combine all together:

// @ts-ignore
type FunctionMap<ArrayFunctions> = UnionToIntersection<ToObjectsArray<ArrayFunctions>[number]>;

Ignore above needed because typescript forgets that it produces array when using mapped type on array type.


OK, let's test:

const arrayToObject = <ArrayFunctions extends ReadonlyArray<any>>(functions: ArrayFunctions) => {
  const result: any = {}

  for (const [name, func] of functions) {
    result[name] = func
  }

  return result as FunctionMap<ArrayFunctions>
}

const arrayFunctions = [
  ['getLength', (text: string) => text.length],
  ['setValue', (id: string, value: number) => { }],
  ['getAll', () => ([1, 2, 3])]
] as const;

const objectFunctions = arrayToObject(arrayFunctions);

const l = objectFunctions.getLength() // Expected 1 arguments, but got 0
objectFunctions.setValue(true, 2) // Argument of type 'true' is not assignable to parameter of type 'string'.

Playground

Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
  • I've tried to convert `type TT = [x: string, y: number]` to `type TT = {x: string, y: number}` by using `type TT = ToObject<[x: string, y: number]>;` but it yields only `type TT = { [x: string]: number; }`. Am I doing something wrong? – danivicario Feb 13 '21 at 08:42
  • @danivicario yes, you have completely different requirement. You have a labeled tuple which is equivalent to `[string, number]` – Aleksey L. Feb 14 '21 at 06:48
  • @AlekseyL. Your solution is amazing and has inspired me to learn more about Typescripts type system. Thank you for that. I wonder if it is possible to constrain the second element of the array to only accept functions? – bennidi Sep 06 '22 at 08:05
  • @bennidi do you mean when we create `const arrayFunctions = ...`? – Aleksey L. Sep 07 '22 at 06:42
  • Yes. That's the part where I am failing to rewrite the typings such that the second element of the tuple is only allowed to be of type XY. – bennidi Sep 07 '22 at 08:00
  • Found a way: ```>(args: T)``` – bennidi Sep 08 '22 at 12:23
0

You can't, Typescript compiler cannot guess types dynamically(at runtime).

In typescript, advanced types like ReturnType or InstanceType are only allowed to guess already defined types.

MJ Studio
  • 3,947
  • 1
  • 26
  • 37
-2

This same problem has been plaguing me for days. Though, with TypeScript@4.4.4, this is possible with a custom typedef:

type ArrayToObject<Arr extends any[]> = {
    [Entry in keyof Arr as string]: Arr[Entry];
};

And you could use like:

type FuncParamsToObj<Func extends (...args: any[]) => any> = ArrayToObject<Parameters<Func>>;

type myFunc = ( a: string, b?: { x?: number, y?: object } ) => string;

const paramObj: FuncParamsToObj<myFunc> = {
    a: 1,
    b: { x: 7, y: { w: 'hi' }},
};
yuyu5
  • 365
  • 4
  • 11