Skip to content

Improve type narrowing when working with generics #55060

Closed
@iscekic

Description

@iscekic

Bug Report

πŸ”Ž Search Terms

type narrowing, generics

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type narrowing

⏯ Playground Link

https://www.typescriptlang.org/play?#code/KYOwrgtgBAou0G8BQUoEEoF4oEYA0KUAQllAExIC+SSALgJ4AOws8ACgIYCWATqcqgDacSADo0AXQBcUAaigAPGfABGwHgG5ClLYWHxRRabMKp6yyGs3at1JAGMA9iADOtKBA4BrYJ16kAHgAVKGAFWlAAExdWSAA+AAoAM143GSC8KBdgJxBImREIPx5BIIkASiw4k1QuJKhk1PdMFtiIcUq5VFyXRwAbYFE+xwBzBOzcyNEFcu0qJCA

πŸ’» Code

enum Enum {
  A = 1,
  B = 2
}

type EnumPair = {
  [Enum.A]: {
    x: number;
  };

  [Enum.B]: {
    y: number;
  };
}

const makePair = <T extends Enum>(first: T, second: EnumPair[T]) => {
  if (first === Enum.A) {
    console.log(second.x)
  }
}

πŸ™ Actual behavior

Property 'x' does not exist on type '{ x: number; } | { y: number; }'.
  Property 'x' does not exist on type '{ y: number; }'.(2339)

πŸ™‚ Expected behavior

I expected typescript to narrow the type of T to the specific enum being used (due to the equality operator), and therefore the enum pair too.

Activity

jcalz

jcalz commented on Jul 18, 2023

@jcalz
Contributor
nmain

nmain commented on Jul 18, 2023

@nmain

But if you called it like this, your proposed narrowing would be invalidated:

makePair<Enum>(Enum.A, { y: 3 });
iscekic

iscekic commented on Jul 18, 2023

@iscekic
Author

But if you called it like this, your proposed narrowing would be invalidated:

makePair<Enum>(Enum.A, { y: 3 });

Calling it appropriately narrows down the type correctly, however I also expected the narrowing to happen inside the if equality block in the function itself.

Since this seems to be a long-standing issue, as well as a duplicate, feel free to close it.

fatcerberus

fatcerberus commented on Jul 18, 2023

@fatcerberus

Calling it appropriately

You're assuming makePair is always going to be called directly with a literal1. Lots of people make this assumption when they write things like T extends Enum or T extends keyof U, but the compiler won't cooperate, because it knows that this can happen:

function getAOrB() { return 0.5 > Math.random() ? Enum.A : Enum.B; }
const enumValue = getAOrB();
makePair(enumValue, { y: 3 });  // not a type error

...and there's no way, at the type level, to prevent that from happening. The lack of narrowing inside the function is therefore intentional--it wouldn't be sound to do so. Hence #27808 which would give you a way to explicitly tell the compiler that T can't be a union of several Enum values and make the above construction invalid.

Footnotes

  1. This is why I dislike using explicit type arguments as a counterexample, as in general the assumption people tend to make is that the unsound cases can never be inferred--which isn't true. ↩

fatcerberus

fatcerberus commented on Jul 18, 2023

@fatcerberus

See also #30581 and related issues, which are fundamentally about a way to communicate to TS that T and EnumPair[T] are correlated when T is a union type.

iscekic

iscekic commented on Jul 18, 2023

@iscekic
Author

The lack of narrowing inside the function is therefore intentional--it wouldn't be sound to do so.

I understand why this is the case for the function body in general, but I don't get how this is true within the strict equality branch.

fatcerberus

fatcerberus commented on Jul 18, 2023

@fatcerberus

See my code example above. Having first === Enum.A doesn’t guarantee you have the correct object for second; the type system isn’t currently powerful enough to make this guarantee.

iscekic

iscekic commented on Jul 18, 2023

@iscekic
Author

I get it - since first is Enum.A | Enum.B and second is EnumPair[Enum.A] | EnumPair[Enum.B], just because I narrowed first doesn't mean second is narrowed too (the co-dependency between the two wasn't communicated to the compiler πŸ˜„).

I worked around the issue by casting second.

Thank you for the explanations everyone! I'm going to close the ticket, since it seems like a duplicate to me.

craigphicks

craigphicks commented on Jul 19, 2023

@craigphicks

An object argument in a template function will have the members appropriately correlated -

enum Enum {
  A = 1,
  B = 2
}
type ArgsA = {
  first: Enum.A,
  second: number;
};
type ArgsB = {
  first: Enum.B,
  second: string;
  third: number;
};
type Args = ArgsA | ArgsB;
const func = <T extends Args>(t:T) => {
  if (t.first === Enum.A) {
    console.log(t.second) // (parameter) second: string
    console.log(t.third) // error
  }
  else if (t.first === Enum.B) {
    console.log(t.second) // (parameter) second: string
    console.log(t.third) // (parameter) third: string
  }
}

func({first:2,second:"",third:3});
func({first:1,second:4,third:3}); // note: extra arg is not an error
func({first:1,second:4,fourk:3}); // note: extra arg is not an error

but as you can see the extra args in the function call are not detected as errors.

Using function overloads will detect the extra args in the function call -

function overloadfunc(t:ArgsA):void;
function overloadfunc(t:ArgsB):void;
function overloadfunc(t:Args):void{
  if (t.first === Enum.A) {
    console.log(t.second) // (parameter) second: string
    console.log(t.third) // error
  }
  else if (t.first === Enum.B) {
    console.log(t.second) // (parameter) second: string
    console.log(t.third) // (parameter) third: string
  }

}

overloadfunc({first:2,second:"",third:3});
overloadfunc({first:1,second:4,third:3}); // error: extra arg IS an error
overloadfunc({first:1,second:4,fourk:3}); // error: extra arg IS an error

playground

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @jcalz@iscekic@fatcerberus@craigphicks@nmain

        Issue actions

          Improve type narrowing when working with generics Β· Issue #55060 Β· microsoft/TypeScript