During my first go at writing this function, I encountered some unexpected behaviour:
function splatIfDefinedBad<K extends string, V>(
key: K,
value: V
): Partial<Record<K, V>> {
if (value === undefined) {
return {}
}
return { [key] : value }
}
The TypeScript compiler emits the error:
Type '{ [x: string]: V & ({} | null); }' is not assignable to type 'Partial<Record<K, V>>'.
I could't figure out where the & ({} | null) part of the type inferred for value was coming from.
Changing the way I construct the object rectifies the problem:
function splatIfDefinedGood<K extends string, V>(
key: K,
value: V
): Partial<Record<K, V>> {
if (value === undefined) {
return {}
}
const output: Partial<Record<K, V>> = {};
output[key] = value;
return output;
}
At first, this implied to me that something about how computed properties work makes it possible for { [key]: value } to produce { [key]: null } even when the type V doesn't contain the value null.
Then I noticed that {} | null means "anything except undefined" (since the type {} means "anything except null or undefined"), so I'm guessing this has something to do with how TypeScript type inference operates? It's trying to narrow V down to exclude undefined but tripping up when it gets V & Not<undefined> (imagining that that mapped type existed) because it can't unify properly? Whereas with the splatIfDefinedGood version, it's going in the reverse direction, adding a property of V at K on a type it already understands, rather than trying to narrow down from "anything" to "anything but undefined"?
EDIT:
As @ruakh points out, the problem is just in K being broadened to string! The following works fine:
function splatIfDefined2<K extends string, V>(
key: K,
value: V
): Partial<Record<K, V>> {
if (value === undefined) {
return {}
}
return { [key as K] : value } as { [k in K]: V & ({} | null) }
}
As @ruakh pointed out in a comment, the problem is just in K being broadened to string! The following works fine:
function splatIfDefined2<K extends string, V>(
key: K,
value: V
): Partial<Record<K, V>> {
if (value === undefined) {
return {}
}
return { [key as K] : value } as { [k in K]: V & ({} | null) }
}
indicating that V & ({} | null) is indeed assignable to V.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With