In the specific example you've provided, you can use ES6 spread syntax to create a shallow clone of an object. This does the same thing your example wants to do:
let newQuote: Quote = {...oldQuote}
You can also do partial updates this way, e.g
let partOfQuote: Partial<Quote> = {
discount: {
totalDiscount: null,
totalAfterDiscount: null
}
}
let newQuote: Quote = { ...oldQuote, ...partOfQuote }
Onto explaining what went wrong:
The type of the expression oldQuote[typedProperty] is Quote[keyof Quote] as you might expect. Its expanded type is string | Discount | null | undefined. That makes sense, we could get any one of those 4 things by using some unknown key to index into a Quote.
However, what will TypeScript let us assign to newQuote[typedProperty]? For type safety reasons, it picks the most restrictive option possible: the intersection of all of the types of the object's values.
As a concrete example, lets say we had this:
interface Person {
firstName: string;
lastName: string;
}
const somePersonKey: keyof Person = "firstName";
const billy: Person = { firstName: "billy", lastName: "johnson" };
Typescript would let us say billy[somePersonKey] = "a string value" because it knows that this is safe for all of the fields of Person. Now, say we updated our code as such to add a field with type number:
interface Person {
firstName: string;
lastName: string;
age: number; // new field
}
const somePersonKey: keyof Person = "firstName";
const billy: Person = { firstName: "billy", lastName: "johnson", age: 67 }
Now, billy[somePersonKey] = "a string value" no longer compiles. TypeScript sees that there is an age field with type number, so if somePersonKey was "age", then the right hand side of the assignment would need to be a number. The only type that is safe for all keys of Person would be a type that is assignable to both string and number. This type is string & number, i.e never, meaning no such values exist.
However, one way to get around this is by using more specific types. If we had another constant
const personNameKey: "firstName" | "lastName" = "firstName";
then we would be allowed to do billy[personNameKey] = "a name", because the type of the firstName field is string, and the type of the lastName field is string, meaning that we can assign strings to billy[personNameKey]. The type of personNameKey tells TypeScript that its value cannot be age i.e we won't run into a problem trying to assign a string to a number field.
In terms of applying this information to fix your loop example, I'm not sure if there is a way. You could have some array like
const quoteKeys = ["recapInfoComments", "discount"] as const
which lets you do newQuote[quoteKeys[0]] = oldQuote[quoteKeys[0]]. Unfortunately, looping over the values of quote keys (e.g for (const key of quoteKeys) { ... } or quoteKeys.forEach((key) => { ... }) does not preserve the specific type, key's type goes back to keyof Person, which is what we were trying to avoid.
Maybe you could get your example to typecheck using a recursive generic function. However, it would be easier to just use an as any typecast and add a comment explaining why doing so is type-safe.
Object.assign(newQuote,oldQuote);?oldQuoteare innewQuotehence thefor inlooplet newQuote:{[key in keyof Quote]:any} = {}and cast it later?