Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent type safety on Unions with nested generic Tuple/Object values. #49082

Closed
Exigerr opened this issue May 12, 2022 · 2 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@Exigerr
Copy link

Exigerr commented May 12, 2022

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.

@jcalz
Copy link
Contributor

jcalz commented 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.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label May 12, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants