This is mostly a known limitation or bug in TypeScript; see microsoft/TypeScript#13948. If you use a computed property in an object literal and that property name isn't known to the compiler to be a single string literal type, then the type of the key is widened all the way to string and you get a string index signature. Until and unless that's fixed you could work around it with a type assertion, or if you're going to do that a lot, wrap the type assertion in a helper function like this:
function kv<K extends PropertyKey, V>(key: K, value: V) {
return { [key]: value } as { [P in K]: { [Q in P]: V } }[K];
}
The kv() function takes a key of type K and a value of type V and returns an object whose type has a property with that key type and that value type. There are all kinds of caveats with this sort of object creation. The "obvious" return type would be Record<K, V> (equivalent to {[P in K]: V}), but if K is a union type then that results in the wrong output. If you call kv(Math.random()<0.5?"a":"b", 123) you want something like {a: number} | {b: number} and not {a: number; b: number}, right? If so, then we need the more complicated { [P in K]: { [Q in P]: V } }[K] type. Let's just test it quickly:
const obj1 = kv("a", 123);
// const obj1: {a: number}
const obj2 = kv(Math.random() < 0.5 ? "a" : "b", 123);
// const obj2: {a: number} | {b: number}
Okay, looks good. Now we can use this helper in your method:
public async appendPresignedUrl<T, K extends PropertyKey>(
value: T,
targetProperty: K,
) {
const presignedUrl = "abc"
return {
...value,
...kv(targetProperty, presignedUrl),
};
}
The type of that method is
/* (method) Foo.appendPresignedUrl<T, K extends PropertyKey>(
value: T, targetProperty: K): Promise<T & {
[P in K]: { [Q in P]: string; }; }[K]> */
Note that I didn't add an extra generic type parameter R there; it would be superfluous to do so, and then the compiler would be rightly concerned that maybe R isn't the type you think it is, since callers choose generic type parameters and not implementers. Instead of R you just need that T & {[P in K]: {[Q in P]: string}}[K] type directly.
Let's test that it works as desired:
const f = new Foo().appendPresignedUrl({ a: 123 }, "hello");
/* const f: Promise<{ a: number; } & { hello: string; }> */
const g = new Foo().appendPresignedUrl({ a: 123 },
Math.random() < 0.5 ? "hello" : "goodbye");
/* const g: Promise<{ a: number; } & (
{ hello: string; } | { goodbye: string; })> */
Looks good!
Playground link to code