Typescript不从Array.includes推断子类型

vq8itlhq  于 2023-06-07  发布在  TypeScript
关注(0)|答案(1)|浏览(121)

我试图让TypeScript使用Array.includes()正确地推断一些变量,但它不起作用。下面是一个简单的例子:

const Array = ['a', 'b', 'c'] as const
type SomeType = {
  a: string
  b: string
  c: string
  foo: boolean
  bar: boolean
}

现在让我们假设我有一个函数,它接收SomeType的键,但实际上我只想在我的键在我的Array中时做一些事情

const myAwesomeFunction = (field: keyof SomeType): void => {
  if (Array.includes(field) doStuff(field) 
}

这里,给doStuff函数的字段类型应该是typeof Array,但实际上是keyof SomeType
我猜这种行为是由于一些模糊的边缘情况,但我想找到一个解决办法,或者至少了解上面代码的根本问题,为什么Typescript认为这是一个错误
我发现了一些关于GH的问题,但我不能真正理解这一切:

jv4diomz

jv4diomz1#

目前,the includes() array method的TypeScript类型不允许它在其输入上充当类型保护。这在microsoft/TypeScript#36275上被提出,但由于太复杂而无法实现而被拒绝。不允许它的原因与includes()返回false时的含义有关。true的结果很容易推理;如果arr.includes(x)true,那么当然x必须与arr的元素具有相同的 * 类型 *。但是如果arr.includes(x)false,那么说x的类型 * 不是 * arr的元素的类型往往是错误的。例如,考虑一下这里会发生什么:

function hmm(x: string | number) {
   if (!([1, 2, 3].includes(x))) {
      x.toUpperCase();
   }
}

xstringnumber类型。如果[1, 2, 3].includes(x)true,那么我们知道xnumber。但如果它是false,我们绝对不能断定xstring。但如果includes()是一个类型保护函数,就会发生这种情况。没有内置的支持说“哦,忽略否定的情况”。这就需要类似于“单侧”或“细粒度”类型保护函数的东西,正如您提到的问题之一microsoft/TypeScript#15048中所要求的那样。
这种变化会立即引起注意和不愉快,因为整个使用TypeScript的世界中的includes()调用会突然看到非常奇怪的效果,它们的搜索值被错误地缩小,可能一直到never类型。
因此,使其成为类型保护的天真方法将是一个问题。我们可以尝试更复杂,只允许函数在像您的代码这样的情况下充当类型保护,其中数组元素类型是文字类型的联合。但这看起来更加复杂和奇怪,涉及conditional typesthis参数,可能如下所示:

interface ReadonlyArray<T> {
   includes(
      this: ReadonlyArray<T extends string ? string extends T ? never : T : never>,
      searchElement: string
   ): searchElement is string & T;
}

它试图检测T是否是字符串文字类型,而不是string本身,然后它使函数充当类型保护。但即使是 that 也不安全,因为没有任何东西要求数组包含其类型的每个元素:

function yuck(x: "a" | "c") {
   const arr: readonly ("a" | "b")[] = ["b"]; // <-- doesn't contain "a"
   if (!(arr.includes(x))) {
      x; // x has been erroneously narrowed to "c"
      ({ c: 123 })[x].toFixed(1); // runtime error
   }
}

这在实践中可能不太可能,但这是一个并发症。即使我们不关心这一点,向全局可用函数添加新的奇怪调用签名也会对其他人的代码产生明显的影响。(推理会用重载函数做一些奇怪的事情,见ms/TS#26591和无数其他函数)。仅仅为了支持这一个用例,不值得所有TypeScript付出努力和风险。
如果你只做一次,我建议你坚持并继续:

const myAwesomeFunction = (field: keyof SomeType): void => {
   if (array.includes(field)) {
      doStuff(field as "a" | "b" | "c")  // assert
   } else {
      field as "foo" | "bar" // assert
   }
}

否则,你可以将它 Package 在你自己的自定义类型保护函数中,并在需要这种行为时使用它:

function myIncludes<T extends U, U>(
  arr: readonly T[], searchElement: U
): searchElement is T {
   return (arr as readonly any[]).includes(searchElement);
}

const myAwesomeFunction = (field: keyof SomeType): void => {
   if (myIncludes(array, field)) {
      doStuff(field)
   } else {
      field // (parameter) field: "foo" | "bar"
   }
}

或者,如果您真的想在代码库中的任何地方看到这种行为,您可以将调用签名合并到Array<T>接口中;这就是如果microsoft/TypeScript#36275被接受的情况,但它不会影响其他任何人。

// declare global { // uncomment this if you're in a module
interface ReadonlyArray<T> {
   includes(
      this: ReadonlyArray<T extends string ? string extends T ? never : T : never>,
      searchElement: string
   ): searchElement is string & T;
}
// } // uncomment this if you're in a module

const myAwesomeFunction = (field: keyof SomeType): void => {
   if (array.includes(field)) {
      doStuff(field)
   } else {
      field // (parameter) field: "foo" | "bar"
   }
}

Playground链接到代码

相关问题