Description
Bug Report
🔎 Search Terms
- union
- tuple
- union infer
- type narrowing
- discriminated union
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about union.
Have checked with Nightly, TS 4 versions and TS 3 versions.
⏯ Playground Link
Playground link with relevant code
💻 Code
interface IContainer<T, V> {
key: T;
value: V;
}
interface ITypeA {
existsOnA: string;
}
interface ITypeB {
existsOnB: number;
}
type ContainerUnion = IContainer<["A"], ITypeA> | IContainer<["B"], ITypeB>;
function test(arg: ContainerUnion): void {};
// ✔ - Passes as expected
test({ key: ["A"], value: { existsOnA: "HELLO" } });
test({ key: ["B"], value: { existsOnB: 0 } });
// ✔ - Errors as expected
test({ key: ["A"], value: { existsOnB: 0 } });
test({ key: ["B"], value: { existsOnA: "HELLO" } });
test({ key: ["A"], value: { existsOnA: "", nonexistentProp: "" } });
// ❌ - Problematic scenario - No errors, but would expect it to.
test({ key: ["A"], value: { existsOnA: "HELLO", existsOnB: 0 } });
🙁 Actual behavior
No type error is occurring for
test({ key: ["A"], value: { existsOnA: "HELLO", existsOnB: 0 } });
//existsOnB does not exist within IContainer<["A"], ITypeA>, but does on IContainer<["B"], ITypeB>.
When matching a type in a union using a nested Tuple or Object for generics, it treats properties as if they are a combination of the different types.
In the example above, once it matches the key
, the type for value
essentially becomes an intersection of the inferred value and partial of all the other permutations.
In essence this
IContainer<["A"], ITypeA> OR IContainer<["B"], ITypeB>;
is being treated as if it was this
IContainer<["A"], ITypeA & Partial<ITypeB>> OR IContainer<["B"], ITypeB & Partial<ITypeA>>;
(you can add more properties onto ITypeB to see this in action)
🙂 Expected behavior
A type error should be thrown for
test({ key: ["A"], value: { existsOnA: "HELLO", existsOnB: 0 } });
due to existsOnB
not being present on the matched part of the union - IContainer<["A"], ITypeA>
.
🗒 Notes
The behavior is currently inconsistent.
-
It will prevent non-existent properties from being valid.
-
It will prevent only properties from other values being valid.
e.g.test({ key: ["A"], value: { existsOnB: 0 } }); //Errors as expected
-
It will allow a partial of the other values.
-
It will work perfectly as expected when the first generics in the union which get matched are primitives (see line 65 onwards of the TS playground link above)
Snippet of a currently working scenario (TS version 4.6):
type SimpleContainerUnion = IContainer<"A", ITypeA> | IContainer<"B", ITypeB>; //Every usage permutation of this works fully as expected.
function test(arg: SimpleContainerUnion): void {};
test({ key: "A", value: { existsOnA: "HELLO", existsOnB: 0 } }); //Works fine - Type error as expected 🎉
I've tried to do a fair bit of digging throughout issues and the documentation, but have not yet come across something which shines a light on this behaviour.
Considering a union of IContainer<"A", ITypeA> | IContainer<"B", ITypeB>
works for all scenarios, but IContainer<["A"], ITypeA> | IContainer<["B"], ITypeB>
or even IContainer<{ type: "A" }, ITypeA> | IContainer<{ type: "B"}, ITypeB>
does not, it feels like a bug.
Apologies if this is a known limitation or bug and I missed it in my earlier searches!
If it is a limitation, a simple explanation or pointer to documentation would be very much appreciated.
Activity
jcalz commentedon May 12, 2022
Duplicate of #20863, I think.
Generics seem to be incidental here; the main issue is that excess property checking does not catch cross-union members for non-discriminated unions.
typescript-bot commentedon May 14, 2022
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.