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

Improve type narrowing when working with generics #55060

Closed
iscekic opened this issue Jul 18, 2023 · 9 comments
Closed

Improve type narrowing when working with generics #55060

iscekic opened this issue Jul 18, 2023 · 9 comments

Comments

@iscekic
Copy link

iscekic commented Jul 18, 2023

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.

@jcalz
Copy link
Contributor

jcalz commented Jul 18, 2023

#24085, #27808, etc

@nmain
Copy link

nmain commented Jul 18, 2023

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

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

@iscekic
Copy link
Author

iscekic commented Jul 18, 2023

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
Copy link

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
Copy link

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
Copy link
Author

iscekic commented Jul 18, 2023

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
Copy link

fatcerberus commented Jul 18, 2023

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
Copy link
Author

iscekic commented Jul 18, 2023

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.

@iscekic iscekic closed this as completed Jul 18, 2023
@craigphicks
Copy link

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants