请注意,TypeScript 不能直接准确地表示这种类型。它可以间接表示为generic 约束,您可以使用辅助函数来要求它,但它增加了复杂性。您可以期望的最好的结果是一种类型,它将您的函数的 用户 限制为传入与此类型匹配的 具体值,然后在任何实现中将类型扩展为某种东西编译器实际上可以轻松推理,例如 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> 在元组类型T 前添加一个类型H,所以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> 是 lookup type T[K] 在编译器不确定K 是否是T 的键的情况下。所以Lookup<{a: string}, "a"> 是string,Lookup<{a: string}, "b"> 是never:
type Lookup<T, K> = K extends keyof T ? T[K] : never;
WidenToStringOrNumberTuple<T> 采用仅包含 string | number 元素的数组或元组类型 T 并将 "a" 或 1 等任何文字扩展为 string 或 number。所以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 类型开始和结束,它必须在number 和string 之间交替,并且元组中必须至少包含三个元素(你没有表明[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] 是不可接受的。计算的核心是映射类型。我们得到Cons<number, Cons<string, WidenToStringOrNumberTuple<T>>> 的I'th 元素。考虑这种类型......它会在T 的扩大到string-或-number 版本的开头附加一个额外的[number, string, ...]。如果T 是["a", 1, "b", "c"],那么它就变成[number, string, string, number, string, string]。所以Ith 元素将是:number for I 是"0"; string for I 是 "1",然后是 T[I - 2] 否则(你不能像那样做类型级算术)。如果I extends keyof Tail<T> 表示I 不是 最后一个索引并且与unknown 相交不会做任何事情(X & unknown 只是X)......但如果它是最后一个索引,我们与number...相交以保证有效路径以number结尾。
很简单,对吧?好吧,也许不是。让我们看看在几个测试用例中发生了什么:
type 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 和 Test4,VerifyAlternator 版本返回与传入内容兼容的类型。但对于 Test2 和 Test3,则不会。
所以,让我们使用它。这是一个辅助函数:
const asAlternator = <T extends (string | number)[]>(
alternator: T & VerifyAlternator<T>): T => alternator;
以下是有效和无效的测试用例:
// works
const okay1 = asAlternator([1, "a", 2]);
const okay2 = asAlternator([1, "a", 2, "b", 3]);
const okay3 = asAlternator([1, "a", 2, "b", 3, "c", 4, "d", 5, "e", 6,
"f", 7, "g", 8, "h", 9, "i", 10, "j", 11, "k", 12, "l", 13, "m", 14, "n", 15,
"o", 16, "p", 17, "q", 18, "r", 19, "s", 20, "t", 21, "u", 22, "v", 23,
"w", 24, "x", 25, "y", 26, "z", 27]);
// errors
const bad1 = asAlternator("a"); // error!
// ~~~ <-- "a" is not (string | number)[]
const bad2 = asAlternator([]); // error!
// ~~ <-- [] is not [number, string, number]
const bad3 = asAlternator(["a"]); // error!
// ~~~ <-- "a" is not number
const bad4 = asAlternator([1]); // error!
// ~~~ <-- [number] is not [number, string, number]
const bad5 = asAlternator([1, "a", true]); // error!
// true is not string | number -> ~~~~
const bad6 = asAlternator([1, "a", "b", 2]); // error!
// "b" is not number ------------> ~~~
const bad7 = asAlternator([1, "a", 2, "b"]); // error!
// "b" is not number --------------> ~~~
如您所见,它支持非常长的有效路径并拒绝无效路径。
那么,就这样吧。类型杂耍成功!复杂性对您来说值得吗?如果是这样,那就太好了。如果不是这样,有限元组联合的折衷方案甚至只是一个未区分的数组可能对您有用。无论如何,您可能都必须进行运行时检查或编译时断言,因为编译器只能理解 VerifyAlternator<T> 的 concrete 值 T,而不是 generic 的值,就像你使用内部函数实现:
function cannotReason<T extends (string | number)[]>(path: T & VerifyAlternator<T>) {
path[2].toFixed(); // error!
// YOU know path[2] is a number, but the compiler still thinks of it as (string | number);
// you'll need to do this:
(path [2] as number).toFixed(); // okay
// or even this:
if (typeof path[2] === "string") throw new Error();
path[2].toFixed(); // okay
}
Link to code