7

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.

3 Answers 3

13

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

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

6 Comments

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 yes, you have completely different requirement. You have a labeled tuple which is equivalent to [string, number]
@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 do you mean when we create const arrayFunctions = ...?
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.
|
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.

Comments

-2

This same problem has been plaguing me for days. Though, with [email protected], 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' }},
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.