如何在TypeScript中定义交替类型的数组?

10
我希望定义一种数组类型,允许在不同位置使用不同类型,但是以某些数据结构中发现的交替、重复的方式进行。
例如:
[A, B, A, B, ...]
[A, B, C, A, B, C, ...]

这是否可能?

我知道我可以像上面那样(没有省略号)为具有固定元素数量的数组定义它。

(A | B)[]

另一方面,它将允许任何元素成为类型 A 或 B 中的任意一种。

我尝试了这些:

[(A, B)...]
[...[A, B]]
[(A, B)*]

你不能这样做。元组按索引具有不同的类型,但长度固定,而数组不允许您定义交替类型。看起来你的数据结构很奇怪,为什么不使用例如 [A, B][],一个元组的数组呢? - jonrsharpe
谢谢。:-( 这是用于 Elasticsearch 批量操作的。 - Arc
1
这是一个很好的问题。希望有一个解决方案。 - rpivovar
这个问题可能是https://dev59.com/4MDqa4cB1Zd3GeqPe30g#69787886的重复。 - captain-yossarian from Ukraine
@Arc,你能否给我发送符合此要求的弹性文档链接? - captain-yossarian from Ukraine
@captain-yossarian来自乌克兰,这是交替执行操作/元数据和源数据(后者是可选的,但在我的情况下,我的批量操作是交替进行的)。不,你提供的链接问题是重复的,而这个问题是更早的 :-) https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html - Arc
2个回答

9

我想到了一个“可行”的东西,但有点疯狂:

type Alternating<T extends readonly any[], A, B> =
  T extends readonly [] ? T
  : T extends readonly [A] ? T
  : T extends readonly [A, B, ...infer T2]
    ? T2 extends Alternating<T2, A, B> ? T : never
  : never

由于递归条件类型的存在,这需要 TypeScript 4.1+ 版本。


朴素用法需要复制值作为字面类型赋给 T 参数,这并不理想:

const x: Alternating<[1, 'a', 2], number, string> = [1, 'a', 2]

这似乎比直接写出 [number, string, number] 作为类型更糟糕。然而,借助于一个虚拟函数,可以避免重复:

function mustAlternate<T extends readonly any[], A, B>(
  _: Alternating<T, A, B>
): void {}

const x = [1, 'a', 2] as const
mustAlternate<typeof x, number, string>(x)
这里有一个带有一些测试用例的实时演示

我实际上不建议在典型代码库中依赖它(使用起来很麻烦,错误消息也很糟糕)。我主要是为了看看类型系统能够被拉伸到多远而一直在努力。

如果有人有关于如何使其更加不稳定的建议,我非常乐意听取!


6

替代方法:

type MAXIMUM_ALLOWED_BOUNDARY = 50

type Mapped<
    Tuple extends Array<unknown>,
    Result extends Array<unknown> = [],
    Count extends ReadonlyArray<number> = []
    > =
    (Count['length'] extends MAXIMUM_ALLOWED_BOUNDARY
        ? Result
        : (Tuple extends []
            ? []
            : (Result extends []
                ? Mapped<Tuple, Tuple, [...Count, 1]>
                : Mapped<Tuple, Result | [...Result, ...Tuple], [...Count, 1]>)
        )
    )



type Result = Mapped<[string, number, number[]]>

// 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48
type Test = Result['length']

/**
 * Ok
 */
const test1: Result = ['a', 42, [1]]
const test2: Result = ['a', 42, [1], 'b', 43, [2]]

/**
 * Fail
 */
const test3:Result = ['a'] // error

const fn = <T, U>(tuple: Mapped<[T, U]>) => tuple

fn([42, 'hello']) // ok
fn([42, 'hello','sdf']) // expected error

Playground

Mapped - 这个名字有些难懂,但是功能很好 :D。创建了一个元组所有允许状态的并集。其中每个元组状态的长度都可以被 3 整除:length%3===0 // true。您可以定义任何您想要的元组,比如 4 个值、5 个值等等。

每次迭代时我都会将 Count 数组增加 1。这就是我知道何时停止递归迭代的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接