如何在 TypeScript 中迭代只读数组并保持数组条目类型?

huangapple go评论75阅读模式
英文:

How to iterate a readonly array while maintaining array entry typing in Typescript?

问题

以下是翻译好的部分:

我尝试严格类型化一个`Map`,以便它只能包含存在于给定的只读数组中的键,我使用以下助手来定义该数组的类型:

```typescript
type ArrayValues<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer ArrayValues>
  ? ArrayValues
  : never;

Map获得了正确的类型,但在对数组进行循环时,我需要手动将其转换为我给定Map的ArrayValues类型。这似乎可能导致错误,因为这可能会将实际上不在数组中的元素进行强制类型转换,因此应该给出错误而不是继续执行。

function test<Buttons extends ReadonlyArray<string | number>>(
  buttonPins: Buttons
) {
  const buttonMap = new Map() as Map<ArrayValues<Buttons>, boolean>;

  buttonPins.forEach((buttonPin) => {
    // 这会引发错误
    // 参数类型为'string | number'无法分配给类型'ArrayValues<Buttons>'。
    // 类型'string'无法分配给类型'ArrayValues<Buttons>'。
    // buttonMap.set(buttonPin, true);
    // 使用强制转换,一切都按我所需的方式工作
    buttonMap.set(buttonPin as ArrayValues<Buttons>, true);
  });

  return buttonMap;
}

完整的示例代码可以在此处找到:

如何在 TypeScript 中迭代只读数组并保持数组条目类型?


**问题是:** 是否有更好的方法来迭代给定的数组(`buttonPins`)以自动修复类型,或者是否有更好的方法来确保我们不会向`buttonMap`中注入错误的值?

我尝试了一些`test`函数的通用类型的不同变化,但没有帮助我更接近解决方案。

<details>
<summary>英文:</summary>

I&#39;m trying to strictly type a `Map`, so that it can only contain keys that are present in the given readonly array, which I type using the following helper:


```typescript
type ArrayValues&lt;T extends ReadonlyArray&lt;unknown&gt;&gt; = T extends ReadonlyArray&lt;infer ArrayValues&gt;
  ? ArrayValues
  : never;

The Map is getting the correct typings, but when looping over the array, I need to add a manual cast to the ArrayValues type I gave the map, this seems like a possibility for errors as this could be casting elements that are not actually inside of the array so it should give me an error instead of going trough.

function test&lt;Buttons extends ReadonlyArray&lt;string | number&gt;&gt;(
  buttonPins: Buttons
) {
  const buttonMap = new Map() as Map&lt;ArrayValues&lt;Buttons&gt;, boolean&gt;;

  buttonPins.forEach((buttonPin) =&gt; {
    // This gives error
    //    Argument of type &#39;string | number&#39; is not assignable to parameter of type &#39;ArrayValues&lt;Buttons&gt;&#39;.
    //      Type &#39;string&#39; is not assignable to type &#39;ArrayValues&lt;Buttons&gt;&#39;.
    // buttonMap.set(buttonPin, true);
    // Using the cast everything works as I want it too
    buttonMap.set(buttonPin as ArrayValues&lt;Buttons&gt;, true);
  });

  return buttonMap;
}

The full sample code can be found here:

如何在 TypeScript 中迭代只读数组并保持数组条目类型?

Question is: Is there a better way to iterate over the given array (buttonPins) to automatically fix the typing, or is there a better way to make sure we don't inject wrong values into the buttonMap?

I've tried some different variations of the generic type of the test function, but none of them helped me get further to a solution.

答案1

得分: 2

您的类型 ArrayValues 可以简单地编写如下:

type ArrayValues<T extends ReadonlyArray<unknown>> = T[number];

由于您在使用具有泛型的条件类型时,TypeScript 懒惰地进行了评估,因此关于 buttonMap.set 的使用是否有效存在一些模糊。

不过,修复方法很简单。使用 Buttons[number] 代替。TypeScript 不会懒惰评估这个,所以它允许您在映射中设置 buttonPin

function test<Buttons extends ReadonlyArray<string | number>>(
  buttonPins: Buttons
) {
  const buttonMap = new Map<Buttons[number], boolean>();

  buttonPins.forEach((buttonPin) => {
    buttonMap.set(buttonPin, true);
  });

  return buttonMap;
}

Playground

另外,Map 接受两个泛型参数。您不需要将其转换为 Map<..., ...>;您可以直接使用 new Map<..., ...>。对于 Set 和它们的弱对应也是如此。

英文:

Your type ArrayValues can be written simply as the following:

type ArrayValues&lt;T extends ReadonlyArray&lt;unknown&gt;&gt; = T[number];

Since you were using a conditional type with a generic, TypeScript was lazily evaluating it and so it gets fuzzy over whether or not the usage of buttonMap.set was valid.

However, the fix is simple. Use Buttons[number] instead. TypeScript won't lazily evaluate this so it allows you to set buttonPin in the map.

function test&lt;Buttons extends ReadonlyArray&lt;string | number&gt;&gt;(
  buttonPins: Buttons
) {
  const buttonMap = new Map&lt;Buttons[number], boolean&gt;();

  buttonPins.forEach((buttonPin) =&gt; {
    buttonMap.set(buttonPin, true);
  });

  return buttonMap;
}

Playground


Another thing, Map takes two generic parameters. You don't need to cast to Map&lt;..., ...&gt;; you can simply use new Map&lt;..., ...&gt;(). This is the same for Set and their weak counterparts.

答案2

得分: 1

type ArrayValues<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer ArrayValues>
  ? ArrayValues
  : never;

function test<Buttons extends ReadonlyArray<string | number>, T extends ArrayValues<Buttons>>(
  buttonPins: readonly T[]
): Map<T, boolean> {
  const buttonMap = new Map<T, boolean>();

  buttonPins.forEach((buttonPin) => {
    buttonMap.set(buttonPin, true);
  });

  return buttonMap;
}

const testArray = ['a', 1, 0, 1, 'test'] as const;
// Map<'a' | 1 | 0 | 'test', boolean>
const testMap = test(testArray);

<details>
<summary>英文:</summary>

You can write it like this (see [playground][1])

```ts
type ArrayValues&lt;T extends ReadonlyArray&lt;unknown&gt;&gt; = T extends ReadonlyArray&lt;infer ArrayValues&gt;
  ? ArrayValues
  : never;

function test&lt;Buttons extends ReadonlyArray&lt;string | number&gt;, T extends ArrayValues&lt;Buttons&gt;&gt;(
  buttonPins: readonly T[]
): Map&lt;T, boolean&gt; {
  const buttonMap = new Map&lt;T, boolean&gt;();

  buttonPins.forEach((buttonPin) =&gt; {
    buttonMap.set(buttonPin, true);
  });

  return buttonMap;
}

const testArray = [&#39;a&#39;, 1, 0, 1, &#39;test&#39;] as const;
// Map&lt;&#39;a&#39; | 1 | 0 | &#39;test&#39;, boolean&gt;
const testMap = test(testArray);

huangapple
  • 本文由 发表于 2023年3月8日 18:26:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/75671835.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定