Skip to content

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

Closed
@Exigerr

Description

@Exigerr

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

jcalz commented on May 12, 2022

@jcalz
Contributor

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

typescript-bot commented on May 14, 2022

@typescript-bot
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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @jcalz@Exigerr@RyanCavanaugh@typescript-bot

        Issue actions

          Inconsistent type safety on Unions with nested generic Tuple/Object values. · Issue #49082 · microsoft/TypeScript