The behavior observed in this scenario closely resembles the situation documented in microsoft/TypeScript#13455. The compiler seems to struggle with assigning concrete values to
Partial<SomethingDependingOnAGenericTypeParam>
, only allowing properties of type
undefined
(seen in the behavior of
obj2
) or an empty object (observed in the behavior of
obj4
) as valid assignments.
This restraint is typically justifiable, as such assignments are often unsafe:
function unsafeExemplified<T extends { key: string }>() {
const nope: Partial<T> = { key: "" }; // Generates CORRECT error
}
interface Oops { key: "someStringValue" };
unsafeExemplified<Oops>(); // {key: ""} is not a valid Partial<Oops>
(this explains the behavior of obj3
). However, this restriction also prevents certain known-safe assignments, like yours that has been specifically crafted:
function safeExemplified<T extends { key: string }>() {
const stillNope: { [X in keyof T]?: X extends "key" ? string : T[X] }
= { key: "" }; // Incorrect error generated
}
(which clarifies the behavior of obj1
). In these instances, it appears that the compiler is lacking the necessary analysis for successful execution. A similar issue (microsoft/TypeScript#31070) was previously resolved as a bug fix, but in your case, the mapped property type involves a conditional type rather than a constant like number
. The compiler already struggles with verifying assignability to conditional types related to unresolved generic parameters. To overcome this limitation, consider using a type assertion to assert your knowledge over the compiler's limitations:
const obj1 = { version: 1 } as Partial<Mix<Versionable, T>>; // Now executes without errors
Interestingly, this restriction eases when writing to a property instead of assigning an object directly to the variable which leads to unsound behavior without any errors:
function unsafeUncaught<T extends { key: string }>(value: Partial<T>) {
value.key = ""; // Unexpected, isn't it?
}
const oops: Oops = { key: "someStringValue" };
unsafeUncaught(oops);
I'm unsure of the specifics behind this decision, but it likely stems from an intentional design choice somewhere. Consequently, the following code snippet does not generate any errors either due to lack of proper validation in the initial phase:
function safeUncaught<T extends { key: string }>(
value: { [Y in keyof T]?: Y extends "key" ? string : T[Y] }
) {
value.key = ""; // Technically okay but coincidental
}
This may explain why version1
, version2
, and version3
function seamlessly, along with the assignment of 1
to obj4.version
.
Hopefully, this information proves helpful. Best of luck!
Code Link Here