4

I'm making a function that adds an additional property to each element of the array:

type AnyObj = { [key: string]: any };

function addIndexProp<T extends AnyObj>(
  obj: T[],
  myProp: string
): T[] {
  return obj.map(item => {
    item[myProp] = 'myProp';
    return item;
  });
}

But I got the following error:

$ tsc --noEmit src/test.ts
src/test.ts:8:5 - error TS2536: Type 'string' cannot be used to index type 'T'.

8     item[myProp] = 'myProp';
      ~~~~~~~~~~~~


Found 1 error.

error Command failed with exit code 1

I don't understand why this happens because I alreay specified T using a string index type, and especially the following code works well:

const test: { [key: string]: any } = { test: 1 };
const myProp = 'prop';
test[myProp] = 'myProp';

Does anyone have an idea why the first snippet throws an error, and how to workaround the problem? Thanks!

2
  • BTW your code will be more readable if you move the inline type-definition to a type statement. Commented Jan 3, 2020 at 2:37
  • @Dai Right. Thanks for pointing me out! Commented Jan 3, 2020 at 2:42

1 Answer 1

5

The problem with T extends AnyObj is that T can be anything to which a value of AnyObj is assignable, and TypeScript will give implicit index signatures to object literal types whose properties match the index signature property. So a type like {a: number} is assignable to AnyObj, despite not having an index signature. So you can write the following code without error:

let obj = { a: 123, b: 345 };
const addedProps = addIndexProp([obj], "a");
addedProps[0].a.toFixed(); // okay at compile time, error at runtime

Inside addIndexProp(), the problematic line ends up assigning a value "myProp" to a property that's supposed to be of type number. And that's not good.


The right fix really depends on your use cases. If you just want to make the compiler happy without actually making your code any safer, you can use a type assertion. This will just let you assign item[myProp]="myProp" even if myProp clobbers existing properties:

function addIndexPropAssert<T extends AnyObj>(
  obj: T[],
  myProp: string
): T[] {
  return obj.map(item => {
    (item as any)[myProp] = 'myProp';
    return item;
  });
}

The safest thing to do is not to mutate the original obj elements and to use more expressive types to indicate that you are adding or overwriting the [myProp] property of T:

function addIndexPropDoNotModify<T, K extends PropertyKey>(obj: T[], myProp: K) {
  return obj.map((item: Omit<T, K>) => expandType(Object.assign(
    { [myProp]: "myProp" } as Record<K, string>,
    item
  )));
}

Here we are using Object.assign() where the target is a new object, so no existing objects get modified. The type will end up being an array of the intersection of Omit<T, K> (meaning T with the [myProp] property removed) with Record<K, string> (meaning the [myProp] property has been added back in with a string value).

And expandType() is just a helper function that convinces the compiler to turn an ugly intersection of mapped types like Omit<{a: number, b: number}, "a"> & Record<"a", string> into a more straightforward type like {a: string, b: number}, like this:

function expandType<T>(x: T) {
  return x as any as T extends infer U ? { [K in keyof U]: T[K] } : never;
}

Here's how it works:

let obj2 = { a: 123, b: 345 }; // {a: number, b: number}
const addedProps2 = addIndexPropDoNotModify([obj2], "a"); // Array<{a: string, b: number}>
addedProps2[0].a.toFixed(); // caught error at compile time, toFixed does not exist on string 

So the difference here is that addedProps2's elements are now seen to be of type {a: string, b: number} even though obj is of type {a: number, b: number}.

Anyway hope that one of those will help give you some direction. Good luck!

Playground link to code

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

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.