0

I'm working on a function that has 1 argument. The argument is either an object (could be nested) or an array of objects. The input can look something like

{
  key1: {
    key2: [{  
      key3: 1, 
      key4: { 
        key5: [...]
      }
    }]
  }
}

or

[
  { key1: 1, key2: 2, key3: { key4: 'val' }},
  { key5: { key6: { key7: 'val' }}}
]

Any part of the input can be an object or array.

For simplicity's sake, this function takes all the keys of the object, and append New to them.

For example, with input

{
  name: 'Name',
  email: '[email protected]'
}

Output would be

{
  nameNew: 'Name',
  emailNew: '[email protected]'
}

If the argument is an array, the function will map through the array and do the same thing.

The issue I'm having is because I don't know the level of nesting for the object, a recursive function is used. Here is what I have so far. Link to playground

interface Object {
  [key: string]: Object | Object[];
}

function mapper(arg: Object[]): Object[];
function mapper(arg: Object): Object {
  if (Array.isArray(arg)) {
    return arg.map(mapper);
  }

  let result: Object = {};
  Object.keys(arg).forEach((key) => {
    let newKey: string = key + "New";
    let value = arg[key];

    if (value instanceof Array) {
      result[newKey] = value.map(mapper);
    } else if (Object.prototype.toString.call(arg) === "[object Object]") {
      result[newKey] = mapper(value);
    } else {
      result[newKey] = value;
    }
  });

  return result;
}

The issue is with this code

  if (Array.isArray(arg)) {
    return arg.map(mapper);
  }

I do a type check of an array, so I would only go into the block if the input is Object[]. When I map through the input and call mapper on each element, Typescript is using the signature

function mapper(arg: Object[]): Object[]

when each element is an Object. and I get this error

Type 'Object[][]' is not assignable to type 'Object'.
  Index signature is missing in type 'Object[][]'.

There are a couple of typing issues, but this is the main one I'm not able to solve.

I'd think Typescript can infer the typing correctly because mapper is called with Object in the map function.

2 Answers 2

1

It is possible that your problem is just that you are not using overloads correctly.


An overloaded function is signified by a set of call signatures seen by the caller of the function. A call signature has no implementation (and is usually followed by a semicolon ;).

If the function has an implementation (and is not just declared), then these call signatures should be followed by a single implementation signature seen by the implementation of the function. The implementation signature has an implementation and is therefore followed by an open curly bracket {.

Note that the implementation signature is not one of the call signatures. The only relationship between the call and implementation signatures is that the implementation must be able to accept the parameters from each call signature and the call signatures must return some subtype of the return type of the implementation signature. This relationship is not completely type safe so you need to be careful with return types. But it's important to note that the call signatures are separate and distinct from the implementation signature.


Anyway, in your original code, you had one call signature of type (arg: Object[])=>Object[], and an implementation signature of type (arg: Object)=>Object. Those are not compatible... and presumably you wanted both of those to be the call signatures and did not remember to provide a separate implementation signature.

I suggest you change both of your signatures to call signatures, and then give it an implementation which accepts as arg a union of the arg types from the call signatures, and returns a union of the return types from the call signatures:

function mapper(arg: Object[]): Object[];
function mapper(arg: Object): Object;
function mapper(arg: Object | Object[]): Object | Object[] {
  // impl goes here
}

This should clear up the errors. Note that the warning about being careful with the return types stands. The compiler is only making sure that you return an Object | Object[]. It will not notice if you return an Object[] when you pass in an Object or vice versa. So take care.

Okay, hope that helps; good luck!

Playground link to code

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

Comments

1

This is an interesting one. This is what I came up with:

interface Object {
  [key: string]: string | Object | Object[];
}

function mapper(arg: Object): Object {
  
  let result: Object = {};
  Object.keys(arg).forEach((key) => {
    let newKey: string = key + "New";
    let value = arg[key];

    if (Array.isArray(value)) {
      result[newKey] = value.map(mapper);
    } else if (typeof value === 'object') {
      result[newKey] = mapper(value);
    } else {
      result[newKey] = value;
    }
  });

  return result;
}


const test: Object = {
  one: "hello",
  three: [{four: "hello"}, {five: "five"}, {six: {seven: "seven"}}]
}

const result = mapper(test);

console.log(result);

  1. Updated the type to include a primitive like a string. In your example some of those values are just primitives, and they would have to be for the recursion to end. You also have this case in the code, but it wasn't reflected in the type.

  2. No need for taking in an array as an argument. The function always takes in a single Object as an argument. So we don't need the first if statement. This can be confusing since some of the values are arrays of objects. However when you .map over those values, you are calling the mapper on a single value at a time, which is always a single Object, never an array of Objects.

  3. I updated some of the if conditions in the core of the function. You may be able to get it to work with the others, but thought this was a bit cleaner.

  4. I included a small test case which I ran in the typescript playground and it appears to give us the right behavior, based on your description but please confirm.

****** Edit ********

Thanks for clarifying. So if you need the function to take both Object and Object[]. The function will also need to return both Object or Object[]. This part can get a bit messy. So you could just add the array logic back to your function. Something like:

if (Array.isArray(arg) {
   return arg.map(obj => mapper(obj));
}

This will throw a type error. Since mapper now can return Object and Object[], as far as typescript is concerned this could be a Object[][], which isn't the type that we want. You can get around this if you typecast like so:

if (Array.isArray(arg) {
   return arg.map(obj => mapper(obj)) as Object[];
}

The typecast we are asserting that this will always be a Object[] which should be true, unless if you wanted to support nested arrays.

That could work, but from a design perspective, maybe write a new function that does the type check upfront. It could be cleaner. Something like:

const mapperMap = (args: Object | Object[]): Object | Object[] => {
    if (Array.isArray(args)) {
      return args.map(obj => mapper(obj));
    }
    return mapper(args);
}

If you set it up this way, you could expand this further to handle other types or edge cases as well. The core mapper function can remain pure and will do one thing and do it well. Take in an Object and return a mapped version of that Object. All other cases can be organized in mapperMap or whatever name makes sense, couldn't think of a better name.

1 Comment

I clarified my question a bit. The input can also be an array. Using your code, I get this error Argument of type 'Object[]' is not assignable to parameter of type 'Object'.

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.