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
typestatement.