I have a simple type union and associated mapped type. I'm trying to write to the mapped type, but in doing so I get this error:
Type '{ foo: T; bar: number; }' is not assignable to type 'Thingies[T]'. Type '{ foo: T; bar: number; }' is not assignable to type 'never'. The intersection 'Thingy<"a"> & Thingy<"b">' was reduced to 'never' because property 'foo' has conflicting types in some constituents.ts(2322)
type Foo = "a" | "b";
type Thingy<T> = {
foo: T;
bar: number;
};
type Thingies = { [K in Foo]: Thingy<K> };
const f: Thingies = {
a: {
foo: "a",
bar: 1,
},
b: {
foo: "b",
bar: 2,
},
};
function assignThingy<T extends Foo>(foo: T) {
f[foo] = {
foo: foo,
bar: 1,
};
}
It seems the issue is that typescript doesn't know that the type of foo
is the same instance of T when used to create the object and assign it to Thingies
. Is there a way to get this to work?
TypeScript can't see that Thingies[K]
and Thingy<K>
are the same type for all K
. (Caveat: that's only true when K
is a single member of the Foo
union, but since that's all we really care about, and the solution also has the same issue, I won't belabor the point). Perhaps it should, but it's not currently supported.
The approach that works is to refactor the types as described in microsoft/TypeScript#47109, to switch from Thingies[K]
to a distributive object type of the form {[P in K]: F<P>}[K]
, which is known to distribute across unions in K
. We can do that like this:
type Thingies<K extends Foo = Foo> =
{ [P in K]: Thingy<P> };
Here Thingies
is now generic. The only change is that we're iterating over K
instead of Foo
. Indeed, the type named Thingies
is Thingies<Foo>
(thanks to the default type argument) and therefore the same as before. But now you can write Thingies<K>[K]
instead of Thingies[K]
and the compiler will follow your intended logic:
function assignThingy<K extends Foo>(foo: K) {
const _f: Thingies<K> = f;
_f[foo] = {
foo: foo,
bar: 1,
};
}
We are allowed to widen from Thingies
to Thingies<K>
for any K extends Foo
, because Thingies
is contravariant in its type argument (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript). TypeScript allows that widening from f
to _f
. And then, _f[foo]
is of type Thingies<K>[K]
, which is now seen as equivalent to Thingy<K>
.