【问题标题】:Define a recursive array of even length in TypeScript在 TypeScript 中定义一个偶数长度的递归数组
【发布时间】:2021-09-23 00:19:05
【问题描述】:

我正在尝试为Mapbox style expressions 的子集编写 TypeScript 类型声明,这是一种基于 JSON 构建的类似 Lisp 的语言。例如:

["+", 1, ["*", 2, 2], 3]
= 8

其中一个运算符是"case",它可以让您编写条件表达式:

["case", true, "yes", "no"]
= "yes"

这需要一对布尔值和值,以及一个备用值(请参阅docs)。

有什么方法可以在 TypeScript 中对“偶数长度的数组”进行建模,尤其是那些元素可能递归引用自身的方法?这是一个尝试:

type Expression = number | string | boolean | CallExpression;

type CallExpression = MathCall | CaseCall;

type MathCall = [
  '+' | '-' | '/' | '*' | '>' | '<',
  Expression,
  Expression,
];

type CaseCallBase = [
  'case',
  Expression,
  ...CaseCallParams,
];

type CaseCallParams = [
  // ~~~~~~~~~~~~~~ Type alias 'CaseCallParams' circularly references itself. (2456)
  Expression,
  Expression,
  ...([] | CaseCallParams)
];

我知道 TypeScript 中有 ways to define a tuple of length N,但我无法避免这些循环引用错误。

这是我提供的最接近的界面,尽管您必须拼出所有字段并将其限制在一定长度。用interface 定义数组感觉很尴尬,并且会导致令人困惑的错误,但我还没有找到任何替代方法。

type Expression = number | string | boolean | CallExpression;

type CallExpression = MathCall | CaseCall;

type MathCall = [
  '+' | '-' | '/' | '*' | '>' | '<',
  Expression,
  Expression,
];

interface CaseCall {
  0: 'case';
  1: Expression;
  2: Expression;
  3: Expression;
  4?: Expression;
  5?: Expression;
  6?: Expression;
  7?: Expression;
  // etc.
  length: 4 | 6 | 8 | 10 | 12; // ...
}

是否可以更准确地定义这种类型并且没有长度上限?

【问题讨论】:

  • 我的预感是目前这是不可能的。我相信一个数组有无限长,或者一个元组有一个数字联合的长度。这些选项都不能满足您的需求。

标签: typescript


【解决方案1】:

我能够生成最大长度为 110 个元素的 55 个元组的联合。 这意味着具有下一个长度的元组:

110 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 76 | 78 | 80 | ... 13 more ... | 108

元组中最多有 110 个元素


type MathCall = [
    '+' | '-' | '/' | '*' | '>' | '<',
    Expression,
    Expression,
];
type Expression = number | string | boolean | MathCall;


type CallParams = [Expression, Expression]

/**
 * It is not allowed to increase this number, at least in TS.4.4
 */
type MAXIMUM_ALLOWED_BOUNDARY = 110

type Mapped<
    Arr extends Array<unknown>,
    Result extends Array<unknown> = [],
    Original extends any[] = [],
    Count extends ReadonlyArray<number> = []
    > =
    (Count['length'] extends MAXIMUM_ALLOWED_BOUNDARY
        ? Result
        : (Arr extends []
            ? []
            : (Arr extends [infer H]
                ? [...Result, H, ...([] | Mapped<Original, [], [], [...Count, 1]>)]
                : (Arr extends [infer Head, ...infer Tail]
                    ? Mapped<[...Tail], [...Result, Head], Arr, [...Count, 1]>
                    : Readonly<Result>
                    )
            )
        )
    )

// Main result
type CaseCallParams = Mapped<CallParams>

// TESTS

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
    U extends any ? (f: U) => void : never
>;

type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
    ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
    : [T, ...A];

type Result = UnionToArray<CaseCallParams>[3]['length']; // 8
type Result_ = UnionToArray<CaseCallParams>[4]['length']; // 10
type Result__ = UnionToArray<CaseCallParams>[10]['length']; // 12
type Result___ = UnionToArray<CaseCallParams>[12]['length']; // 26
type Result____ = UnionToArray<CaseCallParams>[20]['length']; // 42
type Result_____ = UnionToArray<CaseCallParams>[50]['length']; // 102
type Result______ = UnionToArray<CaseCallParams>[54]['length']; // 110

// should end with 0 , 2 , 4 , 6 , 8
type EvenNumbers = UnionToArray<CaseCallParams>[number]['length']

如果它适合你,请告诉我。

Playground

我认为Tail recursive evaluation of conditional types合并后可以增加限制

另外,您可以定义一个验证元组长度的函数:

type EvenEnd = '0' | '2' | '4' | '6' | '8'

type IsEven<T extends `${number}`> =
    (T extends `${infer Int}${infer Rest}`
        ? (
            Rest extends ''
            ? (Int extends EvenEnd
                ? true
                : false
            )
            : (Rest extends `${number}`
                ? IsEven<Rest>
                : false
            )
        )
        : false
    )

{
    type Test1 = IsEven<'80'> // true
    type Test2 = IsEven<'9010'> // true
    type Test3 = IsEven<'1'> // false
    type Test4 = IsEven<'99999999'> // false
}

type EvenLength<T extends Expression[]> =
    IsEven<`${T['length']}`> extends true
    ? T
    : never

const evenTuple = <
    T extends Expression,
    Tuple extends T[]
>(tuple: EvenLength<[...Tuple]>) => tuple

evenTuple([1, 3]) // ok
evenTuple([1, 3, 4]) // error

Playground 2

My article

更新

另一种解决方案,它允许您创建具有 999 元素的元组。

type Expression = number | string | boolean | CallExpression;

type CallExpression = MathCall | CaseCall;

type MathCall = [
    '+' | '-' | '/' | '*' | '>' | '<',
    Expression,
    Expression,
];

type MAXIMUM_ALLOWED_BOUNDARY = 999

type Mapped<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : Mapped<N, [...Result, Result['length']]>
    )

// 0 , 1, 2 ... 998
type NumberRange = Mapped<MAXIMUM_ALLOWED_BOUNDARY>[number]


type Dictionary = {
    [Prop in NumberRange as `${Prop}`]: Prop
}

type EvenEnd = '0' | '2' | '4' | '6' | '8'

type IsEven<T extends `${number}`> =
    (T extends `${infer Int}${infer Rest}`
        ? (
            Rest extends ''
            ? (Int extends EvenEnd
                ? true
                : false
            )
            : (Rest extends `${number}`
                ? IsEven<Rest>
                : false
            )
        )
        : false
    )

type Compare<Num extends number> =
    Num extends number
    ? IsEven<`${Num}`> extends true
    ? Num
    : never
    : never

type EvenRange = Exclude<Compare<NumberRange>, 0>

type CaseCall<Exp = any> = {
    [Prop in Exclude<NumberRange, 0>]?: Exp
} & { length: EvenRange }

const tuple: CaseCall<Expression> = [1, 1, 1, 1] as const // ok
const tuple2: CaseCall<Expression> = [1, 1, 1] as const // expected error


const handle = <
    Exp extends Expression, Data extends Exp[]
>(
    arg: [...Data],
    ...check: [...Data]['length'] extends EvenRange ? [] : [never]
) => arg

handle([1, 1, 1]) // expected error
handle([1, 1]) // ok

Playground

更新 2

创建长度均匀的元组联合的更简单方法:


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]>

// 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24
type Test = Result['length']

Playground

【讨论】:

    猜你喜欢
    • 2017-06-06
    • 2017-06-09
    • 2017-04-29
    • 2022-01-10
    • 2021-11-20
    • 1970-01-01
    • 2022-01-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多