user18458709
user18458709

Reputation: 11

How to write this generic function in TypeScript which merges array parameter and values into an object?

I have written a function, which merges a keys array and a values array into an object. The implementation is just like below:

function mergeToObject(keys: string[], values: string[]) {
  const object:? = {}
  for (let i = 0; i < keys.length; i++) {
    object[keys[i]] = values[i] // TypeScript complains it
  }
  return object
}

For illustrating the function, it behaves like below:

mergeToObject(['a', 'b', 'c'], ['x', 'y', 'z']) // => { a: 'x', b: 'y', c: 'z' }

And also, I always pass keys param as a constant expression like ['a', 'b', 'c'], but not a runtime value. so I guess it must exists a generic syntax exactly declaring the return type contains keys a, b and c.

For illustrating the behavior more concretely:

const values = ['x', 'y', 'z']

// If I invoke as
const object = mergeToObject(['a', 'b', 'c'], values)
// Then the type of object should be `{ a: string, b: string, c: string }`
object.a // is ok
object.b // is ok
object.c // is ok
object.d // is not ok

// In other words, if I invoke as
const object = mergeToObject(['e', 'f', 'g'], values)
// Then the type of object should be `{ e: string, f: string, g: string }`
object.e // is ok
object.f // is ok
object.g // is ok
object.d // is not ok

So what is exactly the generic written? I'll sincerely appreciate your help.

Upvotes: 0

Views: 29

Answers (3)

Tobias S.
Tobias S.

Reputation: 23825

The generic solution to this problem would look like this:

function mergeToObject<
  K extends string[], 
  V extends any[],
  RT extends { 
    [I in keyof K as K[I] extends string ? K[I] : never]: V[Exclude<I, number> & keyof V] 
  }
>(keys: readonly [...K], values: readonly [...V]): RT {

  const object: Record<string, any> = {}
  for (let i = 0; i < keys.length; i++) {
    object[keys[i]] = values[i]
  }
  return object as RT
}

We store the keys and the values arrays in the generic tuple types K and V. The return type of the function will be stored in RT. For RT we map over K and use K[I] as the key name for each property. V[I] will be used as the type for each property. Since TypeScript does not know that V can be indexed with I, we have to use this syntax: V[Exclude<I, number> & keyof V].

Some tests to see if it's working:

const object = mergeToObject(['a', 'b', 'c'], ['x', 23, new Date()])
//    ^? const object: { a: string; b: number; c: Date; }

object.a
object.b
object.c
object.d // Error: Property 'd' does not exist on type '{ a: string; b: number; c: Date; }'



const object2 = mergeToObject(['e', 'f', 'g'], [23, 'y', undefined])
//    ^? const object2: { e: number; f: string; g: undefined; }

object2.e
object2.f
object2.g
object2.d // Error: Property 'd' does not exist on type '{ e: number; f: string; g: undefined; }

Playground

Upvotes: 0

DecPK
DecPK

Reputation: 25408

SOLUTION 1

You can also create an interface and assing it to it as:

interface mergeObject {
  [k: string]: string
}

which means that mergeObject is an object of key and value as string

interface mergeObject {
  [k: string]: string
}

function mergeToObject(keys: string[], values: string[]): mergeObject {
  const object: mergeObject = {}
  for (let i = 0; i < keys.length; i++) {
    object[keys[i]] = values[i] // TypeScript complains it
  }
  return object
}

const result = mergeToObject(['a', 'b', 'c'], ['x', 'y', 'z']) // => { a: 'x', b: 'y', c: 'z' }
console.log(result)

SOLUTION 2

interface mergeObject {
  [k: string]: string;
}

function mergeToObject(keys: string[], values: string[]): mergeObject {
  return keys.reduce((acc: mergeObject, curr: string[], index: number) => {
    acc[curr] = values[index];
    return acc;
  }, {});
}

const result = mergeToObject(['a', 'b', 'c'], ['x', 'y', 'z']); // => { a: 'x', b: 'y', c: 'z' }
console.log(result);

Upvotes: 0

Mario Vernari
Mario Vernari

Reputation: 7304

Use Record:

function mergeToObject(keys: string[], values: string[]) : Record<string, any> {
  const object: Record<string, any> = {}
  for (let i = 0; i < keys.length; i++) {
    object[keys[i]] = values[i] // TypeScript complains it
  }
  return object
}

More info here: https://www.typescriptlang.org/docs/handbook/utility-types.html#example-3

Note: I used any as type for the values, but if you prefer to constrain to string only, just change to it.

Upvotes: 1

Related Questions