ts-reset icon indicating copy to clipboard operation
ts-reset copied to clipboard

Narrow types for certain cases of Array.includes

Open schummar opened this issue 2 years ago • 2 comments

As discussed in #49 a simple : searchElement is T won't do, because the assertion is too strong and produces wrong types, especially in the "opposite" cases (e.g. else blocks).

My suggestion is to reenable the type guard only for cases where it is safe: Tuple with well known entries - so only entries that are literals without unions.

For example this is safe, because x is 1 | 2 | 3 if and only if the array contains it:

const values = [1, 2] as [1, 2] // or as const
declare const x: 2 | 3;

if (values.includes(x)) {
  console.log(x);
  //          ^? // 2
} else {
  console.log(x);
  //          ^? // 3
}

Known problem

There is one remaining problem that I know of, where the type guard would do the wrong thing and I cannot imagine a way to prevent this. But depending on how you look at it, it might be ok, since it seems to me that this is actually due to a wider TypeScript problem (bug?)

If the array itself is a union:

const values2 = [1, 2] as [1, 2] | [2, 3]
declare const x2: 3 | 4

if (values2.includes(x2)) {
  console.log(x2);
  //          ^? // 3
} else {
  console.log(x2);
  //          ^? // 4 <-- wrong! if x is 3 we would still hit the else case
}

But the same problem arises with other type guards as well:

const f1 = (x: number): x is 1 => x === 1;
const f2 = (x: number): x is 2 => x === 2;
const f = f1 as typeof f1 | typeof f2;
const n = 2 as 1 | 2 | 3;

if (f(n)) {
  console.log(n);
  //          ^? // 1 | 2
} else {
  console.log(n);
  //          ^? // 3 <-- wrong!
}

Playground

schummar avatar Mar 30 '23 14:03 schummar

Damn this is good, I ignored the else behavior totally, seems like it works (for the last example) like

type ElsePath = Exclude<1 | 2 | 3, 1 | 2>
//   ^? 3

@schummar was this reported as a bug?

MiroslavPetrik avatar Nov 08 '23 14:11 MiroslavPetrik

@schummar was this reported as a bug?

Not that I know of, but it's hard to tell, since there are so many open issues in the TypeScript repo. Maybe I should create one...

schummar avatar Nov 08 '23 15:11 schummar

@schummar Thanks for the PR! Because of the downsides you pointed our in your description, I'll close it. You're right - there's no way around it. Type predicates can be a bit dodgy for that reason, so I want to avoid adding too many of them to ts-reset.

mattpocock avatar May 27 '24 16:05 mattpocock

Fair enough. Thanks for looking into it.

schummar avatar May 27 '24 20:05 schummar