如何键入长度未知的有序类型的数组?

时间:2019-06-07 13:36:00

标签: typescript

我正在使用路径(由数字和字符串组成的数组)来指定特定对象在嵌套数组和对象结构中的位置。

在每种情况下,路径都将始终以数字开头和结尾,并且之间具有任意数量的交替字符串和数字对。

(即[数字,字符串,数字] | [数字,字符串,数字,字符串,数字] | [数字,字符串,数字,字符串,数字,字符串,数字])

我知道元组允许使用有序类型,但是我不知道数组会持续多久,因为我不知道嵌套结构的深度。

2 个答案:

答案 0 :(得分:2)

警告TypeScript无法直接 准确地表示这种类型。可以将其间接表示为generic约束,您可以使用助手函数进行约束,但这会增加复杂性。您可以期望的最好的方法是,将类型为 users 的函数约束为传入与该类型匹配的 concrete值,然后在任何实现中将类型扩展为某种形式编译器实际上可以轻松地进行推理,例如Array<string | number>

作为一种折衷,您说您不知道需要多长时间一个元组,但是我敢打赌,有一个合理的最大值,对吗?您是否期望这些元组需要几十个元素长?如果您可以想到一个合理的最大值,则可以使用示例类型进行折衷:

type PickAReasonableMaximum = 
  [number, string, number] |
  [number, string, number, string, number] | 
  [number, string, number, string, number, string, number] |
  [number, string, number, string, number, string, number, string, number] | 
  [number, string, number, string, number, string, number, string, number, string, number]
// ... etc

但是,因为我喜欢疯狂的类型变戏法,所以我将尝试通过通用约束来表示这一点。完成后,我们将拥有类似VerifyAlternator<T>的类型,其中如果T是有效的路径类型,则T将可分配给它,而T not 是有效的路径类型,则T not 可分配给它(实际上,VerifyAlternator<T>表示有效的路径类型或接近有效值,因此用户在“不良”部分会收到错误消息)。然后,您将拥有一个像function asAlternator<T>(x: T & VerifyAlternatorT): T;这样的辅助函数,它仅返回其输入,但是如果输入未通过验证,则会抛出编译器警告。

首先,让我们提出一些类型操作别名:

Tail<T>将采用元组类型T并返回删除了第一个元素的元组。因此Tail<[1,2,3]>应该是[2,3]

type Tail<T extends any[], D = never> =
  ((...args: T) => never) extends ((a: any, ...args: infer R) => never)
  ? R
  : D;

Cons<H, T>在元组类型H之前加上了类型T,因此Cons<1,[2,3]>应该是[1,2,3]

type Cons<H, T extends any[]> = 
  ((h: H, ...t: T) => any) extends ((...x: infer X) => any) ? X : never;
如果编译器不确定Lookup<T, K>是否是T[K]的键,则

Klookup type T。因此Lookup<{a: string}, "a">string,而Lookup<{a: string}, "b">never

type Lookup<T, K> = K extends keyof T ? T[K] : never;

WidenToStringOrNumberTuple<T>采用仅包含T元素的数组或元组类型string | number,并将诸如"a"1之类的任何文字扩展为stringnumber。所以WidenToStringOrNumberTuple<[string, number, "a", 1]>[string, number, string, number]

type WidenToStringOrNumberTuple<T extends (string | number)[]> = { [I in keyof T]:
  T[I] extends string ? string : T[I] extends number ? number : never
};

呈现主要景点VerifyAlternator<T>。这将采用字符串和数字的元组类型,并返回有效的(或更接近有效的)T的交替路径版本。约束是(据我从您的问题中了解到的),元组必须以number类型开始和结束,它必须在numberstring之间交替,并且该元组必须至少具有其中三个元素(您没有表示[number]可以)。好吧,这里:

type VerifyAlternator<T extends (string | number)[]> =
  T['length'] extends 0 | 1 ? [number, string, number] :
  { [I in keyof T]: Lookup<Cons<number, Cons<string, WidenToStringOrNumberTuple<T>>>, I> &
    (I extends keyof Tail<T> ? unknown : number)
  }

第一位照顾[][number]是不可接受的。计算的内容是映射类型。我们得到I的第Cons<number, Cons<string, WidenToStringOrNumberTuple<T>>>个元素。考虑那种类型...它将一个额外的[number, string, ...]粘贴到string的扩大到numberT版本的开头。如果T["a", 1, "b", "c"],则它变成[number, string, string, number, string, string]。因此,第I个元素为:number的{​​{1}}为I"0"的{​​{1}}是string,然后是类似I的东西(否则无法进行类型级别的算术运算)。如果"1"表示T[I - 2]不是最后一个索引,并且与I extends keyof Tail<T>相交则没有任何作用(I只是{{1 }})...但是如果最后一个索引,则我们与unknown ...相交以保证有效路径以X & unknown结尾。

简单,对吧?好吧,也许不行。让我们看看在一些测试案例中会发生什么:

X

如您所见,对于numbernumbertype Test1 = VerifyAlternator<[1, "a", 2]>; // type Test1 = [number, string, number]; // matches type Test2 = VerifyAlternator<[1, "a", "b"]>; // type Test2 = [number, string, number]; // doesn't match type Test3 = VerifyAlternator<[1, "a", 2, 3]>; // type Test3 = [number, string, number, string & number]; // doesn't match type Test4 = VerifyAlternator<[1, "a", 2, "b", 3]>; // type Test4 = [number, string, number, string, number]; // matches 版本返回与传入的内容兼容的类型。但是对于Test1和{{1} }并非如此。

所以,让我们使用它。这是一个辅助函数:

Test4

这是一些行之有效的测试用例:

VerifyAlternator

如您所见,它支持非常长的有效路径,并拒绝无效路径。


所以,您去了。类型杂耍成功!复杂性值得吗?如果是这样,那太好了。如果不是这样,那么元组有限联合的折衷甚至是未分化的数组都可以为您服务。无论如何,您可能都必须进行运行时检查或编译时间断言,因为对于Test2 concrete 值,编译器仅理解Test3,对于通用的 而言,编译器仅理解em>,就像您在内部函数实现中使用的一样:

const asAlternator = <T extends (string | number)[]>(
  alternator: T & VerifyAlternator<T>): T => alternator;

好的,希望能有所帮助。祝你好运!

Link to code

答案 1 :(得分:0)

您可以使用union type,例如

type PathMember = string | number;
type Path = PathMember[];
let myPath: Path = [0, "left", 2, "alleyway"];

但是,这并没有将开始和结束成员强制为数字。