使用可选的first和last元素定义元组列表

时间:2019-05-10 22:02:29

标签: typescript ecmascript-6

我正在尝试在TypeScript中建模如下数据:

// Sometimes we lookup the min/max dynamically, so we have keywords for them.
const intervalA: NumericInterval = [['$min', 3], [3, 5], [5, '$max']];

// Other times, we know the min/max and provide them inline
const intervalB: NumericInterval = [[0, 3], [3, 5], [5, 7]];

我尝试将NumericInterval定义为:

type MinInterval = ['$min', number];
type MaxInterval = [number, '$max'];
type Interval = [number, number];
type NumericInterval = [MinInterval?, ...Interval[], MaxInterval?];

但是,TypeScript不喜欢它是因为A rest element must be last in a tuple

有没有更好的方式来表达这种模式?

2 个答案:

答案 0 :(得分:2)

据我所知,没有办法直接表示这种类型。它是具体类型Array<["$min" | number, number | "$max"]>的子类型,它太宽了,并且允许诸如[[2, "$max"],[4, "$max"],["$min", 6]]之类的东西。

可以使用genericmappedconditional类型来表示所需的形状作为对数组类型的约束,但这非常难看/乏味/ complex,那么您必须使产生或接受该类型的任何东西都是通用的。我不妨给出一种方法来做,而无需太多解释(如果您确实关心或希望使用此解决方案,我总是可以进行更详尽的解释):

// get the tail of a tuple: Tail<[1,2,3]> is [2,3]
type Tail<L extends any[]> = ((...x: L) => any) extends
    ((h: any, ...t: infer T) => any) ? T : never;

// verify that T is a valid NumericInterval
type VerifyNumericInterval<T> = T extends Array<any> ?
    { [K in keyof T]: [
        K extends '0' ? "$min" | number : number,
        K extends keyof Tail<T> ? number : number | "$max"
    ] } : Array<["$min" | number, number | "$max"]>

// helper function to ensure parameter is a valid NumericInterval
const asNumericInteral = <T extends any[] | [any]>(
    numericInterval: T & VerifyNumericInterval<T>
): T => numericInterval;

让我们对其进行测试:

asNumericInteral([]); // okay, zero length tuple
asNumericInteral([[1, 2]]); // okay
asNumericInteral([["$min", 2]]); // okay
asNumericInteral([[1, "$max"]]); // okay
asNumericInteral([["$min", "$max"]]); // okay, not sure if you want it to be
asNumericInteral([["$max", 2]]); // error!
//                 ~~~~~~ <-- string not assignable to never
asNumericInteral([[1, 2], [3, "$max"]]); // okay
asNumericInteral([["$min", 2], [3, "$max"]]); // okay
asNumericInteral([["$min", 2], [3, "$max"], [5, 6]]); // error!
//                                 ~~~~~~ <-- string not assignable to number

这一切都符合我对该类型的期望。顺便说一下,这仅适用于期望NumericInterval类型的函数的调用者。在具有通用类型T & VerifyNumericInterval<T>值的内部实现中,您可能必须自己处理边缘情况。编译器几乎不可能推理出未解决的泛型类型,以至于足以引起注意,例如:

function hmm<T>(numInt: T & VerifyNumericInterval<T>) {
    for (let i = 0; i < numInt.length; i++) { // okay, numInt is known to be aray
        const interval = numInt[i]; // interval is type [number | "$min", "$max" | number]
        if (i !== 0) { // can't be "$min", right?
            interval[0].toFixed(); // error?! 
            // but it has to be a number, why doesn't the compiler know it?!
        }

        // manually check
        if ((i !== 0) && (typeof interval[0] === "number")) {
            interval[0].toFixed(); // okay now
        } 
    }
}

在该函数中,您知道除i === 0之外,numInt[i]是一对,其中第一个元素肯定是number。但是编译器无法解决问题,因此您必须通过额外的检查(或使用类型断言)来帮助它。

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

答案 1 :(得分:0)

也许您可以考虑将其表示为这样的地图:

type min = "$min" | number;
type max = "$max" | number;

const NI = new Map<min, max>([["$min", 3], [2,3], [1,2], [5, "$max"]]);

console.log([...NI]);

或者,您可以使数组更深一层,以便在元组中进行中间扩展。

type all = [MinInterval[], Interval[], MaxInterval[]];

const min: MinInterval = ["$min", 3];
const max: MaxInterval = [5, "$max"];
const interval: Interval[] = [[5, 3], [3, 4]];
const a: all = [[min], [...interval], [max]];

console.log(a.flat(1));