How to tell TypeScript to understand that using array.some() in an if clause already narrows type?

48 Views Asked by At

I have this code:

const list = ['a', 'b', '= c', 'd']
if (list.some(element => element.includes('='))) {
    const elementWithEqualSign = list.find(element => element.includes('='))
}

In here elementWithEqualSign has type string | undefined, while in fact it should be only string, due to the condition above filtering the possibility of undefined. Is there a way to tell it to automatically narrow the type without using type assertion as string? I suppose the answer is somewhere in TypeScript: Documentation - Narrowing, but I don't know to find it out.

3

There are 3 best solutions below

2
Roger Lipscombe On

In the general case, no.

Consider the situation where you use a different condition in the .some() from the one in .find().

How would the compiler figure out (automatically) that the first condition does/does not change the return type of .find().

What if the condition is arbitrarily complicated? What if, for example, it checked each element against a REST API? Even with an apparently identical condition, it could return a different result the second time (this is sometimes referred to as time-of-check-time-of-use -- TOCTOU), meaning that undefined is a possible type.

Use a type assertion.

0
Dimava On

Okay don't do this but
https://tsplay.dev/mLJBZw

interface Array<T> {
  // make "some" to say it has the item
  some<V>(predicate: (v: T) => v is T & V): this is { guaranteedHas: V };
  // make "find" to get the item if it has it
  find<V>(this: { guaranteedHas: V }, predicate: (v: T) => v is T & V): V;
}
interface ReadonlyArray<T> {
  // make "some" to say it has the item
  some<V>(predicate: (v: T) => v is T & V): this is { guaranteedHas: V };
  // make "find" to get the item if it has it
  find<V>(this: { guaranteedHas: V }, predicate: (v: T) => v is T & V): V;
}
interface String {
  // this is a "proper" typing, but...
  includes<T, V extends string>(this: T, searchString: V): this is T & `${string}${V}${string}`;
}

function includes<D extends string>(d: D) {
  // ...but you need a wrapper because `is` returns become booleans
  return function inc<S extends string>(s: S): s is S & `${string}${D}${string}` {
    return s.includes(d)
  }
}

const list = ['a', 'b', '= c', 'd'] as const
if (list.some(includes('='))) {
  const elementWithEqualSign = list.find(includes('='))
  //    ^?
  // const elementWithEqualSign: "= c"
}
3
mbojko On

Why go through the array twice with the same test? Use find for both:

const elementWithEqualSign = list.find(element => element.includes('='));

if (elementWithEqualSign) {
// elementWithEqualSign is a string here
}