microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.08k stars 12.37k forks source link

Filter with "something is smallerType" don't work as negative #58996

Open Noriller opened 2 months ago

Noriller commented 2 months ago

πŸ”Ž Search Terms

filter is type narrow negative

πŸ•— Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.5.2#code/C4TwDgpgBAggdgSwLYEMA2UC8UDeAoKKAIwCcIVgALALimBIFcIAaPAXwG489RIoAlAPYBjANZZcBYmQo0oAM3QBnFuy49w0AMqCkEKgjgBzCfGTooAHwEjR6+QzjDgCQXCgIlZ1GgAUS3X1KQyNaHT0DYwBKWgCI4OMPJVhEH0lCMmAGEnc4oJCAOlJyKnZuYTclYDoIKoBGMMDIk2x8QmLZWnomVjZyyurgWuAAJkb4kIk26RK5RTQVXvUKuCq6YKUASSUAeXFsAG0h+uYaqpGAXQL5BDQhkl9qzAA+JO90R6iorgB6H8IAYQAHoAfhS5jQBwu-VWgw22wA6iQ3C0oEdhnVTsdRlcbncIA8nq8AISed5+YBfX7-QHAsG+clWGxiKJQvBAA

πŸ’» Code

type Animal = {
  breath: true,
};

type Rock = {
  breath: false,
};

type Something = Animal | Rock;

function isAnimal(something: Something): something is Animal {
  return something.breath
}

const test1: Something = {
  breath: true,
}

const test2: Something = {
  breath: false,
};

const thisIsOk = [test1, test2].filter(t => isAnimal(t));
//       ^? Animal[]

const thisIsWrong = [test1, test2].filter(t => !isAnimal(t));
//       ^? (Animal | Rock)[]

πŸ™ Actual behavior


const thisIsOk = [test1, test2].filter(t => isAnimal(t));
//       ^? Animal[]

const thisIsWrong = [test1, test2].filter(t => !isAnimal(t));
//       ^? (Animal | Rock)[]

πŸ™‚ Expected behavior


const thisIsOk = [test1, test2].filter(t => isAnimal(t));
//       ^? Animal[]

const thisIsShouldBe = [test1, test2].filter(t => !isAnimal(t));
//       ^? Rock[]

Additional information about the issue

Filtering is working only if "positive", but if the "is" is used as a negative then it don't type narrow.

jcalz commented 2 months ago

This is independent of filter():

function positive(t: Something) {
  return isAnimal(t)
}
// function positive(t: Something): t is Animal

function negative(t: Something) { 
  return !isAnimal(t)
}
// function negative(t: Something): boolean

I'm surprised that #57465 didn't do this. Inside negative() it certainly seems that !isAnimal(t) narrows in the way we expect:

function negative(t: Something) {
  if (!isAnimal(t)) {
    ((t));
    //^? (parameter) t: Rock
  } else {
    ((t));
    //^? ^?(parameter) t: Animal
  }
  return !isAnimal(t)
}
fatcerberus commented 2 months ago

@jcalz I don't think there's anything #57465 could have done to help here. We don't have negated types so you can't say t is not Animal--not even explicitly.

jcalz commented 2 months ago

But we don't need t is not Animal, only t is Rock.

Noriller commented 2 months ago

Actually, it should be Exclude<Something, Animal> because in the case we add another one, we want all except the one we are filtering out.

https://www.typescriptlang.org/play/?ts=5.6.0-dev.20240624#code/C4TwDgpgBAggdgSwLYEMA2UC8UDeAoKKAIwCcIVgALALimBIFcIAaPAXwG489RIoAlAPYBjANZZcBYmQo0oAM3QBnFuy49w0GGgQQ4E-IVLkqtOAzRpWnbr2gBlQUghUEcAOYT4ydFAA+AiLiAdq6cOryDHDCwAiC+ghK3qhoABRKTi6Ubu60js6uHgCUtBkF2R5QibCIKZKEZMAMJPplWTkAdMayWJjY9ExcbNzC8UrAdBDjAIx5mYWe2IbSJnIDqsN4o3Djk+MATHPlOQZS3aYKyhvq27vAU8AAzEftlUtnMhfmltY3YxOuJQASSUAHlxNgANr3GbMPbAfZwmFPAC6HXkCDQ9xIqQmmAAfFUkrV0LiikUuAB6SmEWmEAB6AH4aj40JCUSN-nRssClAB1EjxRZQaEPaZIh6I+GPNEYrEQHF4wkAQkSyVJwHJVJpdIZzNS6owASEYn8sB0eiK7KgAFobYSlJRBBYACbEaCpE3Bc1hK0c7jUqAATSmcKBAHIkFAUAB3FBkKBAqBKBimihQVCiE4QAAeKCQYDQUw6QA

jcalz commented 2 months ago

I certainly didn't mean that t => !isAnimal(t) should return t is Rock no matter what Something is, that would be bonkers. Yes, I'd expect Exclude<Something, Animal>, which in your example code was Rock.