If you have a value of type Partial<T> and just copy all of its properties to a value of type T as-is, then you might find yourself erroneously copying an undefined value into a property that doesn't expect it (e.g., if --exactOptionalPropertyTypes is not enabled).
So it's probably good to get an error here, at least in the case where you don't check for undefined. Of course you also get an error when you do check for undefined, because TypeScript's ability to reason about generic types isn't very good. It doesn't see Partial<T>[K] & ({} | null) as assignable to T[K], even though it is (I think? My ability to reason about generic types might also not be very good?)
Since TypeScript's type system isn't fully sound, it's always possible to fool the compiler into allowing you to do unsafe things. In some sense, anything you do that works around this is no safer than using the non-null assertion operator) as you've done here:
function foo<T>(input: Partial<T>, instance: T) {
for (const key in input) {
if (typeof input[key] !== "undefined") {
instance[key] = input[key]!;
}
}
}
I don't think there's anything wrong with this, since it conveys your intent fairly clearly to any future developer: you check for undefined and then assert that it's not undefined.
However, if you want to avoid a type assertion, you could take advantage of some unsoundness by widening instance from T to Partial<T>. The language takes the position that objects are covariant in their property types (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript ) and since every property of T is assignable to the corresponding property of Partial<T>, then it sees T as assignable to Partial<T>. That gives you this code:
function foo<T>(input: Partial<T>, instance: T) {
const widenedInstance: Partial<T> = instance;
for (const key in input) {
if (typeof input[key] !== "undefined") {
widenedInstance[key] = input[key];
}
}
}
Now there's no error. But, it's not any safer than your other code, really. Objects are only really covariant in their read-only properties. As soon as you start writing to properties, then covariance is unsafe. The above code happens to be safe because I'm testing for undefined, but I hadn't, or if I had made a mistake and tested the wrong way, there would still be no error:
if (typeof input[key] === "undefined") { // oops, no error
widenedInstance[key] = input[key];
}
So you have to be careful. And since you have to be careful here no matter what you do, it's essentially a matter of opinion whether either of the two approaches is better, or whether some third approach might be even better.
TypeScript's unsoundness is there intentionally, because a fully sound type system for TypeScript is "simply a fool's errand" and would TypeScript almost unusable, so even if you try to rewrite the code to be as verified as safe as possible by the compiler (maybe using generics for the key type as well as the object so that you are assigning T[K] directly instead of T[keyof T], etc) it will almost certainly allow unsafe things to happen in edge cases. You might as well go for ergonomic code that works for you... which again, is subjective.
Like, here's a potential third approach:
for (const key in input) {
const i: T[Extract<keyof T, string>] | undefined = input[key]
if (typeof i !== "undefined") {
instance[key] = i;
}
}
All I've done here is widen your i to the type the compiler requires for instance[key] = i to work once undefined is eliminated. Is that better? ♂️
Playground link to code