Typescript 获取联合类型的有效组合

huangapple go评论63阅读模式
英文:

Typescript get valid combination of union types

问题

NumberOfChildren = "0" | "1" | "2" | "3+"
EldestAge = "-1" | "0-3" | "3-10" | "11-17" | "18+" // -1 for null
NextEldestAge = "-1" | "0-3" | "3-10" | "11-17" | "18+" // -1 for null

如果我执行${NumberOfChildren}_${EldestAge}_${NextEldestAge}

我会得到无效的组合,例如:

0_0-3_-1 // 不能有年龄为0-3的最年长的孩子
2_3-10_18+ // 下一个最年长的孩子不能比最年长的还要老

我尝试过使用条件类型,比如:

type combos<count extends string, age1 extends string, age2 extends string> =
| ([count, age1, age2] extends ["0", "-1", "-1"] ? "0_-1_-1" : "0_-1_-1")
| ([1, age1, -1] extends ["1", age1, "-1"] ? "1_${age1}_-1" : "0_-1_-1")
.... more combos

但是唯一的结果是"0_-1_-1"

正确的方法是什么?最终目标是一个包含所有有效组合的联合类型,用下划线连接起来。

英文:

Say I have union types like:

NumberOfChildren = "0" | "1" | "2" | "3+"
EldestAge = "-1" | "0-3" | "3-10" | "11-17" | "18+" // -1 for null
NextEldestAge = "-1" | "0-3" | "3-10" | "11-17" | "18+" // -1 for null

If I do ${NumberOfChildren}_${EldestAge}_${NextEldestAge}

I will get invalid combinations like:

0_0-3_-1 // Can't have an Eldest age with no children
2_3-10_18+ // Next Eldest can't be older than eldest

I've tried doing conditional types like:

type combos<count extends string, age1 extends string, age2 extends string> =
| ([count, age1, age2] extends ["0", "-1", "-1"] ? "0_-1_-1" : "0_-1_-1")
| ([1, age1, -1] extends ["1", age1, "-1"] ? "1_${age1}_-1" : "0_-1_-1")
.... more combos

But the only thing that comes out is "0_-1_-1".

What's the right way to do this? End goal is a union type with all valid combinations of the types joined with a _

EDIT: Based on vera's comment this is a solution:

type NumberOfChildren = "0" | "1" | "2" | "3+"
type AgeGroups = ["-1", "0-3", "3-10", "11-17", "18+"] // -1 for null

type Builder<Age extends string, AgeGroup extends string[], Previous extends string[] = []> =
    AgeGroup extends [infer Head extends string, ...infer Tail extends string[]]
        ? `${Age}_${Head}_${Exclude<Head | Previous[number], "-1">}` | Builder<Age, Tail, [...Previous, Head]>
        : never;


type ZeroChildren = `0_-1_-1`;
type OneChild = `1_${AgeGroups[number]}_-1`;
type TwoChildren = Builder<"2", AgeGroups>
type ThreeOrMoreChildren = Builder<"3", AgeGroups>;

type Test = TwoChildren;
//   ^?

type ValidCombos = ZeroChildren | OneChild | TwoChildren | ThreeOrMoreChildren;
//   ^?

答案1

得分: 1

使用元组来描述我们的年龄分组会使实现变得更容易:

type AgeGroups = ["-1", "0-3", "3-10", "11-17", "18+"];

让我们分别处理所有不同的情况:0个孩子,1个孩子,2个孩子和3个或更多孩子。虽然你可以编写一些复杂的类型来处理所有情况,但我认为分别处理它们更易于阅读和维护。

对于零个孩子,我们只能硬编码一个情况:

type ZeroChildren = `0_-1_-1`;

接下来是一个孩子,也非常简单:

// 排除第一个孩子为“-1”
type OneChild = `1_${Exclude<AgeGroups[number], "-1">}_-1`;

但是对于两个孩子,我们需要更多的逻辑,因为第二个孩子必须比第一个孩子年龄小(或者相同):

type TwoChildren<A extends string[] = AgeGroups, Previous extends string[] = []> =
    A extends [infer Head extends string, ...infer Tail extends string[]]
        ? `2_${Head}_${Exclude<Head | Previous[number], "-1">}` | TwoChildren<Tail, [...Previous, Head]>
        : never;

由于 AgeGroups 是一个元组,我们可以“迭代”它。在每次迭代中,我们存储已经迭代过的所有先前元素。然后在下一次迭代中,我们可以在结果中使用先前的元素(仍然确保排除“-1”)。

三个或更多孩子应该与两个孩子处理方式相同:

type ThreeOrMoreChildren = TwoChildren;

因此,当你完成后,你将得到

type ValidCombos = ZeroChildren | OneChild | TwoChildren | ThreeOrMoreChildren;

可以简化为

type ValidCombos = ZeroChildren | OneChild | TwoChildren;

Playground

英文:

Using a tuple to describe our age groups would make the implementation a little easier:

type AgeGroups = [&quot;-1&quot;, &quot;0-3&quot;, &quot;3-10&quot;, &quot;11-17&quot;, &quot;18+&quot;];

Let's handle all the different cases separately: 0 children, 1 child, 2 children, and 3 or more. While you could write some complex type to do it all, I think that covering them separately is easier to read and maintain.

For zero children, there is only one case which we can hardcode:

type ZeroChildren = `0_-1_-1`;

Next is one child, which is also pretty simple:

// exclude -1 as first child
type OneChild = `1_${Exclude&lt;AgeGroups[number], &quot;-1&quot;&gt;}_-1`;

But for two children, we need a lot more logic, since the second child must be younger (or the same age) as the first:

type TwoChildren&lt;A extends string[] = AgeGroups, Previous extends string[] = []&gt; =
    A extends [infer Head extends string, ...infer Tail extends string[]]
        ? `2_${Head}_${Exclude&lt;Head | Previous[number], &quot;-1&quot;&gt;}` | TwoChildren&lt;Tail, [...Previous, Head]&gt;
        : never;

Since AgeGroups is a tuple, we can "iterate" over it. Every iteration we store all the previous elements we already iterated over. Then in the next iteration, we can use the previous elements in the result (still making sure to exclude "-1").

Three or more children should be handled the same as two children:

type ThreeOrMoreChildren = TwoChildren;

So when you're done you'll end up with

type ValidCombos = ZeroChildren | OneChild | TwoChildren | ThreeOrMoreChildren;

which can be simplified to

type ValidCombos = ZeroChildren | OneChild | TwoChildren;

Playground

huangapple
  • 本文由 发表于 2023年2月13日 22:42:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/75437363.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定