0

You can check the typescript playground for the code the issue is with the newQuote.discount nested object can't be assigned. Getting error Type 'null' is not assignable to type '(string & Discount) | undefined .

type Quote = {
  recapInfoComments?: string | null
  discount?: Discount

}

type Discount = {
  totalDiscount?: number | null
  totalAfterDiscount?: number | null
}

const oldQuote: Quote = {
  recapInfoComments: "kugadsfg",
  discount: {
    totalDiscount: 34,
    totalAfterDiscount: 234,
  }
}

let newQuote = {} as Quote

for (const property in oldQuote) {
  const typedProperty = property as keyof Quote
  newQuote[typedProperty] = oldQuote[typedProperty]

}
3
  • How about using Object.assign(newQuote,oldQuote);? Commented Aug 13, 2021 at 15:46
  • Its part of a larger codebase where only parts of oldQuote are in newQuote hence the for in loop Commented Aug 13, 2021 at 15:50
  • Start with let newQuote:{[key in keyof Quote]:any} = {} and cast it later? Commented Aug 13, 2021 at 15:53

2 Answers 2

2

In this case you know more than ts compiler, so you can use as any when assigning the value:

for (const property in oldQuote) {
  const typedProperty = property as keyof Quote
  newQuote[typedProperty] = oldQuote[typedProperty] as any;
}

PlaygroundLink

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

Comments

1

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.

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.