I'm adding a new property value:"4"
to the object Foo
, I have two approches in aaa
and bbb
. When hovering over them, both show the same expected result:{ name:string; value:"4";}
but there is an error in type bbb
saying: Type 'k' cannot be used to index type 'Foo'
type Foo={
name:string
}
type aaa={
[k in keyof Foo|"value"]:k extends keyof Foo?Foo[k]:"4"
}
type bbb={
[k in keyof Foo|"value"]:k extends "value"?"4":Foo[k]
^^^^^^
}
Why the error? Is it that the conditional in aaa
being true
constrains k
so that Foo[k]
become valid?
This is a missing feature of TypeScript, requested at microsoft/TypeScript#48710. If you have a generic type X
which is constrained to another type Y
, then a conditional type like X extends Z ? T<X> : F<X>
the type of X
is re-constrained to Y & Z
in the true branch T<X>
, but it is not re-constrained to something like "Y
but not Z
" in the false branch.
In general it would not be correct to do so, since there are situations in which X extends Y
is true, X extends Z
is false, but X extends Y-but-not-Z
is also false. For example:
type F<X extends { x: string | number }> =
X extends { x: string } ? X['x']['toUpperCase'] :
X['x']['toFixed']; // <-- error!
That's flagged as an error for good reason; because you could evaluate something like:
type G = F<{ x: 0 | "a" }>
where {x: 0 | "a"} extends {x: string | number}
is true, and {x: 0 | "a"} extends {x: string}
is false, but {x: 0 | "a"} extends {x: number}
is also false.
So it's not considered a bug to fail to re-constrain the false branch, because it's not always warranted.
Still, in situations like this where the generic type is constrained to a union of literal types and you are checking it against some subset of that union, and the generic type is only ever going to be a single member of that union (e.g., it's a distributive conditional type or you are iterating over a set of known keys in a mapped type), then it would be safe to do it. It just hasn't been implemented. So it's a missing feature.
For now the workarounds are to reorder your conditional type as you've shown above, or do a possibly redundant second check:
type BBB = {
[K in keyof Foo | "value"]:
K extends "value" ? "4" :
K extends keyof Foo ? Foo[K] : never;
}