microsoft / TypeScript

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

Can't infer array.prototype.find as not undefined when array.prototype.some is true in the upper if clause #60473

Open NagayamaToshiaki opened 2 days ago

NagayamaToshiaki commented 2 days ago

🔎 Search Terms

find some undefined

🕗 Version & Regression Information

I tried TS Playground with nightly.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.0-dev.20241110#code/GYVwdgxgLglg9mABMGAnAzlAMgUylHVACigENUBzPdALkU1RjAoG0BdASkQG8AoRRDGCIS5KlHQA6dHAC2OUZTyIAvAD5EZJVEkAbHMygALRBoCMHDnwECA9LfpMIOTWOqIjpAG4uZ8xDj68mASiADuRnDoLvqGJuQusnCoLsakSGYANMhw4AAmiLIgmIhgcFCIAEYuAOT5OChgOHk1VTgQpMUuAA4peTAdBILo9KTy-DYQCCXAuWAFKq7aUo15RIriqhpa4noGFMamiBYcANwTdg6VIBWz+YUwmKQA1ga6AJ6CYMCEI1Bw9CgjGYAB96o1mpojMUcrpdHAwkwKIgUlAQKgkMZUAiRoRsagLii8OikHd5iwAAxsc4CAC+E1RJMQACJmedaUA

💻 Code

function firstLetter(targets: string[]) {
  if (targets.some(target => target.length > 1)){
    // since targets have some elements whose length are more than 1, found must not be 'undefined' because predicate is same
    const found = targets.find((target => target.length > 1));
    // but found mistakenly infers to string|undefined thus following return throws error
    return found[0];
  }
  return "";
}

🙁 Actual behavior

found[0] throws error "'found' is possibly 'undefined'.(18048)" Because some and find has same predicate and some is true, find must return string.

🙂 Expected behavior

The code can be compiled without error.

Additional information about the issue

No response

guillaumebrunerie commented 2 days ago

There is no way TypeScript can guess that. For instance if your function was not pure (for instance it calls Math.random), then it wouldn't be correct. And trying to detect which functions are pure is already way out of scope for TypeScript.

A workaround is to simply use find:

function firstLetter(targets: string[]) {
  const found = targets.find((target => target.length > 1));
  if (found !== undefined){
    return found[0];
  }
  return "";
}

Although this doesn't work in the same way if your array sometimes contains undefined itself.

MartinJohns commented 2 days ago

For instance if your function was not pure (for instance it calls Math.random), then it wouldn't be correct.

That's not really the issue. TypeScript does not handle pureness at all, you can call functions that modify your narrowed array and you end up with wrong types.

The issue here is that find() does not know about the some() call earlier, and that logically an element is always found. There's no special logic for the find/some pair, and there's no suitable type for some() to narrow to that would allow this logic without special hardcoded behavior (not going to happen).

Duplicate of #47404.

jcalz commented 2 days ago

This isn't a bug in TypeScript, nothing is behaving in a way it's not supposed to. At worst this is a design limitation, although I'm not sure why you'd want to iterate an array twice if you can just do it once.

Anyway, I think this is the closest you can get to that behavior without TypeScript having to implement a bunch of new features:

type LongEnough = string & { __longEnough: true };
const isLongEnough = (target: string): target is LongEnough =>
  target.length > 1

interface FindableArray<T, S extends T> extends Array<T> {
  find(predicate: (value: T, index: number, obj: T[]) => value is S, thisArg?: any): S
}

// merge in an overload for some():
interface Array<T> {
  some<S extends T>(
    predicate: (value: T, index: number, array: T[]) => value is S,
    thisArg?: any
  ): this is FindableArray<T, S>
}

function firstLetter(targets: string[]) {
  if (targets.some(isLongEnough)) {
    const found = targets.find(isLongEnough);
    return found[0];
  }
  return "";
}

Essentially you'd need to convince TypeScript that your predicates act as type guards, since that's the only way two predicates can be reasonably considered "the same". But TypeScript doesn't have a type that means "string of at least 2 characters" (well, it has `${string}${string}${string}` but that collapses to string weirdly so let's ignore that). So then we need to make a nominal-ish LongEnough type and say that your predicate narrows its input from string to LongEnough. Then we need to merge into the Array<T> definition an overload for some() that, when it encounters a type predicate callback, narrows the array to a FindableArray where we know there's at least one element of the narrowed type that you'll find(), and then... blegggh.