Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does TypeScript infer this strange type for the value of a computed property? [duplicate]

Tags:

typescript

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) }
}
like image 717
PoolOfPeas Avatar asked Oct 11 '25 23:10

PoolOfPeas


1 Answers

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.

like image 161
PoolOfPeas Avatar answered Oct 16 '25 09:10

PoolOfPeas