TypeScript支持负值的IntRange

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

TypeScript IntRange which supports negative values

问题

以下是代码的翻译部分:

declare type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
    ? Acc[number]
    : Enumerate<N, [...Acc, Acc['length']]>

declare type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>

关于你的问题,当使用负数范围时,会出现以下错误:

TS2589: Type instantiation is excessively deep and possibly infinite.

要支持在 TypeScript 中使用负数范围,你可以将类型 IntRange 修改如下:

declare type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
    ? Acc[number]
    : Enumerate<N, [...Acc, Acc['length']]>

declare type IntRange<F extends number, T extends number> = F extends 0
    ? Exclude<Enumerate<T>, Enumerate<0>>
    : Exclude<Enumerate<T>, Enumerate<F>> | Exclude<Enumerate<0>, Enumerate<F>>

通过这个修改,你应该能够在 TypeScript 中支持负数范围了。

英文:
declare type Enumerate&lt;N extends number, Acc extends number[] = []&gt; = Acc[&#39;length&#39;] extends N
    ? Acc[number]
    : Enumerate&lt;N, [...Acc, Acc[&#39;length&#39;]]&gt;

declare type IntRange&lt;F extends number, T extends number&gt; = Exclude&lt;Enumerate&lt;T&gt;, Enumerate&lt;F&gt;&gt;

This works for integer ranges like IntRange&lt;25, 200&gt; but when I give it a negative like IntRange&lt;-25, 200&gt; I get this error:

TS2589: Type instantiation is excessively deep and possibly infinite.

How can I support integer ranges in TypeScript with negative numbers?

答案1

得分: 1

TypeScript不原生支持数值范围类型;当前对这一功能的讨论可以在microsoft/TypeScript#54925找到,目前尚不清楚是否会实现。在这一功能得到实现之前,我们需要使用数字字面类型联合来表示整数范围。

此外,TypeScript不原生支持对数字字面类型的数学运算;当前对此功能的请求可以在microsoft/TypeScript#26382中找到。在此功能得到实现之前,我们需要使用绕过的方法来让类型系统计算这些事情。

我知道两种使类型系统计算数字字面类型上的数学操作的方法:

  • 操纵tuple types并检查它们的length属性。例如,向元组类型附加或前置元素相当于增加,连接元组类型相当于加法。这相对简单,但将域限制为相当有限的非负整数。您的Enumerate实用程序类型就是这样工作的。

  • 操纵template literal types并更精确地推断解析字符串字面类型。这允许您将字符串字面类型转换为数字字面类型,反之亦然,因此您可以通过序列化和反序列化数字的Number.prototype.toString(10)表示来设计基于字符串的数学操作的实现。这可能会变得复杂/丑陋,但允许您处理大数字、负数、分数等等。

因此,要使您的IntRange<F, T>能够处理负数,您至少需要在某种程度上使用模板字面类型。以下是一个取负数的方法:

type Negate<N extends number> =
  N extends 0 ? 0 :
  `${N}` extends `-${infer S extends number}` ? S :
  `-${N}` extends `${infer S extends number}` ? S :
  never;

type X = Negate<1.23>;
// type X = -1.23
type Y = Negate<-1.2e-26>;
// type Y = 1.2e-26

这通过解析数字的字符串表示,并添加或删除初始的"-"字符来工作。

您可以将其与Enumerate结合使用,如下所示:

type NonNegativeIntRange<F extends number, T extends number> =
  Exclude<Enumerate<T>, Enumerate<F>>;

type NegativeIntRange<F extends number, T extends number> =
  Exclude<Negate<NonNegativeIntRange<Negate<T>, Negate<F>>> | F, T>;

type MixedIntRange<F extends number, T extends number> =
  F | Negate<Enumerate<Negate<F>> | Enumerate<T>>;

type IntRange<F extends number, T extends number> =
  `${F}` extends `-${string}` ?
  `${T}` extends `-${string}` ?
  NegativeIntRange<F, T> :
  MixedIntRange<F, T> :
  NonNegativeIntRange<F, T>;

有三种有效的方式来调用IntRange<F, T>:要么FT都是负数,都是非负数,要么F是负数而T是非负数(第四种可能性,即F是非负数而T是负数,是无效的,因为F必须小于或等于T)。因此,我们只需检查我们处于哪种情况,然后委托给NonNegativeIntRange(您的原始实现)、NegativeIntRange(将F更改为Negate<T>并将T更改为Negate<F>,调用NonNegativeIntRange<F, T>,然后对结果进行Negate)或MixedIntRange(在Negate<F>T上使用Enumerate并处理端点)。

让我们进行测试:

type X = IntRange<25, 30>;
//   ^?type X = 25 | 26 | 27 | 28 | 29

type Y = IntRange<-30, -25>;
//   ^?type Y = -30 | -26 | -27 | -28 | -29

type Z = IntRange<-2, 3>;
//   ^?type Z = 0 | 1 | 2 | -2 | -1

这将按您提出的问题的要求工作。请注意,Enumerate是一种尾递归条件类型,因此它仅适用于TF的数量小于1000的情况。

如果您希望它适用于更大数量的情况,但仍不需要T减去F大于1000(或999或其他什么)的情况,那么您将需要放弃Enumerate的元组类型,并使用模板字面类型来实现整数增减操作,可能如下所示:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

type Next = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
type Inc<T extends number> =
  T extends -1 ? 0 :
  `${T}` extends `-${infer N extends number}` ? `-${Dec<N>}` extends
  `${infer M extends number}` ? M : never :
  `${T}` extends `${infer F extends number

<details>
<summary>英文:</summary>

TypeScript does not natively support *numeric range types*; the current open discussion of this feature is [microsoft/TypeScript#54925](https://github.com/microsoft/TypeScript/issues/54925), and it&#39;s not clear that this will ever be implemented.  Until and unless this happens, we&#39;ll need to use [unions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) of numeric [literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) to represent integer ranges.

Furthermore, TypeScript does not natively support *mathematical operations on numeric literal types*; the current open feature request for that is [microsoft/TypeScript#26382](https://github.com/microsoft/TypeScript/issues/26382).  Until and unless this happens, we&#39;ll need to use roundabout methods to get the type system to compute these things.

The two ways I know of to make the type system compute math-like operations on numeric literal types are:

* manipulation of [tuple types](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) and inspection of their `length` properties.  For example, appending or prepending elements to tuple types corresponds to incrementation, and concatenating tuple types corresponds to addition.  This is fairly straightforward, but limits the domain to non-negative integers of a fairly modest size.  Your `Enumerate` utility type works this way.

* manipulation of [template literal types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types) and [more precise inference of parsed string literal types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-8.html#improved-inference-for-infer-types-in-template-string-types).  This lets you convert string literal types to numeric literal types and vice versa, and thus you can devise string-based implementations of mathematical operations, by serializing and deserializing the [`Number.prototype.toString(10)` representation of numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString).  This can be complicated/ugly, but lets you potentially deal with large numbers, negative numbers, fractional number, etc.

In order for your `IntRange&lt;F, T&gt;` to work with negative numbers, therefore, you&#39;ll need to use template literals types, at least in part.  Here&#39;s a way to negate a number:

    type Negate&lt;N extends number&gt; =
      N extends 0 ? 0 :
      `${N}` extends `-${infer S extends number}` ? S :
      `-${N}` extends `${infer S extends number}` ? S :
      never;
    
    type X = Negate&lt;1.23&gt;;
    // type X = -1.23
    type Y = Negate&lt;-1.2e-26&gt;
    // type Y = 1.2e-26

That works by parsing the string representation of a number and either peeling off or adding on an initial `&quot;-&quot;` character.

You can use that with `Enumerate` as follows:

    type NonNegativeIntRange&lt;F extends number, T extends number&gt; =
      Exclude&lt;Enumerate&lt;T&gt;, Enumerate&lt;F&gt;&gt;

    type NegativeIntRange&lt;F extends number, T extends number&gt; =
      Exclude&lt;Negate&lt;NonNegativeIntRange&lt;Negate&lt;T&gt;, Negate&lt;F&gt;&gt;&gt; | F, T&gt;;

    type MixedIntRange&lt;F extends number, T extends number&gt; =
      F | Negate&lt;Enumerate&lt;Negate&lt;F&gt;&gt;&gt; | Enumerate&lt;T&gt;

    type IntRange&lt;F extends number, T extends number&gt; =
      `${F}` extends `-${string}` ?
      `${T}` extends `-${string}` ?
      NegativeIntRange&lt;F, T&gt; :
      MixedIntRange&lt;F, T&gt; :
      NonNegativeIntRange&lt;F, T&gt;;

There are three valid ways to call `IntRange&lt;F, T&gt;`: either `F` and `T` are both negative, both non-negative, or `F` is negative and `T` is non-negative (the fourth possibility, where `F` is non-negative and `T` is negative, is invalid, because `F` must be less than or equal to `T`).  So we just check which of those situations we&#39;re in, and then delegate either to `NonNegativeIntRange` (your original implemenation), `NegativeIntRange` (change `F` to `Negate&lt;T&gt;` and `T` to `Negate&lt;F&gt;`, call `NonNegativeIntRange&lt;F, T&gt;`, and then `Negate` the result), or `MixedIntRange` (use `Enumerate` on `Negate&lt;F&gt;` and `T` and fiddle with endpoints).

Let&#39;s test it:

    type X = IntRange&lt;25, 30&gt;;
    //   ^?type X = 25 | 26 | 27 | 28 | 29
    
    type Y = IntRange&lt;-30, -25&gt;;
    //   ^?type Y = -30 | -26 | -27 | -28 | -29
    
    type Z = IntRange&lt;-2, 3&gt;;
    //   ^?type Z = 0 | 1 | 2 | -2 | -1

So that will work for your question as asked.  Note that `Enumerate` is a [tail-recursive conditional type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#tail-recursion-elimination-on-conditional-types) so it will necessarily only work situations where `T` and `F` have magnitudes less than 1000.

----

If you wanted it to work for larger magnitudes but still don&#39;t need situations where `T` minus `F` is bigger than 1000 (or 999 or something) then you&#39;ll need to abandon `Enumerate`&#39;s tuple types and implement an integer increment/decrement operation with template literal types.  Maybe like this:

    type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

    type Next = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
    type Inc&lt;T extends number&gt; =
      T extends -1 ? 0 :
      `${T}` extends `-${infer N extends number}` ? `-${Dec&lt;N&gt;}` extends
      `${infer M extends number}` ? M : never :
      `${T}` extends `${infer F extends number}${Digit}` ?
      `${T}` extends `${F}${infer D extends Digit}` ?
      `${D extends 9 ? Inc&lt;F&gt; : F}${Next[D]}` extends
      `${infer N extends number}` ? N : never : never :
      T extends 9 ? 10 : Next[T]

    type Prev = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8];
    type Dec&lt;T extends number&gt; =
      `${T}` extends `-${infer N extends number}` ? `-${Inc&lt;N&gt;}` extends
      `${infer M extends number}` ? M : never :
      `${T}` extends `${infer F extends number}${Digit}` ?
      `${T}` extends `${F}${infer D extends Digit}` ?
      `${D extends 0 ? Dec&lt;F&gt; extends 0 ? &quot;&quot; : Dec&lt;F&gt; : F}${Prev[D]}` extends
      `${infer N extends number}` ? N : never : never :
      T extends 0 ? -1 : Prev[T]

These are conditional types, where `Inc` on a negative number resolves to `Dec` and vice versa, and where the last digit is incremented or decremented and then, when necessary, a carry or borrow operation is performed on the previous digit recursively.  Then `Intrange` can be defined simply enough as

    type IntRange&lt;F extends number, T extends number, A extends number = never&gt; =
      F extends T ? A : IntRange&lt;Inc&lt;F&gt;, T, F | A&gt;

where you just increment from `F` to `T`.   Let&#39;s test it:

    type X = IntRange&lt;25, 30&gt;;
    //   ^?type X = 25 | 26 | 27 | 28 | 29

    type Y = IntRange&lt;-30, -25&gt;;
    //   ^?type Y = -30 | -26 | -27 | -28 | -29

    type Z = IntRange&lt;-2, 3&gt;;
    //   ^?type Z = 0 | 1 | 2 | -2 | -1

    type W = IntRange&lt;12345678, 12345703&gt;
    // type W = 12345678 | 12345679 | 12345680 | 12345681 | 12345682 | 12345683 | 12345684 | 12345685 | 
    //    12345686 | 12345687 | 12345688 | 12345689 | 12345690 | 12345691 | 12345692 | 12345693 | 12345694 | 
    //    12345695 | 12345696 | 12345697 | 12345698 | 12345699 | 12345700 | 12345701 | 12345702

    type V = IntRange&lt;-987654321, -987654315&gt;
    // type V = -987654321 | -987654320 | -987654319 | -987654318 | -987654317 | -987654316

That works even for relatively large numbers, although it would fail spectacularly for numbers written in scientific notation, or fractions, etc.

----

Handling arbitrarily large numbers would require more work.  So would handling ranges where `T` minus `F` is bigger than a thousand but less than about a hundred thousand.  Handling ranges where `T` minus `F` is bigger than a hundred thousand would require native support for numeric range types, since unions cannot hold more elements than about a hundred thousand.

[Playground link to code](https://www.typescriptlang.org/play?noErrorTruncation=true#code/HYQwtgpgzgDiDGEAEApeIA2AvJBvAUEkqJLAsgKLACukATiAC4D2dehRSjAnjJTfSYQAPADkkEAB6MIwACZRitAEYQ6AGiQBBCdNkKlYVXQDaAXSQBeJOYB8Vjp20mA5BlkBzRgAsXFqTLyiuIA-M4CxmaOnABcSFS0akJimiYAdBlamlqu7sBevmZmttE8fEiiEB7J4gH6ihFq9pbRRLV6QUgADEhhPTGtSAAGACS4ogC+Q7qBBkMAtGMAlsAAZmpIAMoz9YbGU71bSANORAtjk9N1naO4K+ts29cGjXQHYdsnp8AQAG5qAG58KVeMgABqSKwVKrJACMaQATABmWxApwAenRXFBSAhUPm8ORIPKAE1uNwoZVqjJhATERB5giAGwlDFYsrIElQwkM5nApwcirMYBUphLf4ASWAjAASiB8iIAGI7TqvTQAFRVLxUTQcp3ikngGGochECUENPVtk05qSNMVthKxOQosY4ogUtl8o8Sq1DR1GiQmue-qMupa+oohuNprEMJpomFrvdnrlCrj1JEVs0rqVjvsAB8kIqNaj+ZxBQBZJaSCByVPe30hvZqDV+lt0ZqDZVF3PCW0MBPxvOOpBFgfJK3A51IBvp5XNtVB9uvLv626Kg7N864KCMOgrDzvQa3dVbjpzRa7-eH4-65OS6Vpn3CEtB+xfJzV2v1p+N1+lscgyJiK8Ypn+86lkCM5glCc4vgiACsmhIl0ZZspwAB6ISCrB1hIWOSDMoRCIAOwkQAHCRACc5ZEIKXLWPBIjzKhmiMoh6GcJiWE4TijFIKxPRFoyTKEYy5EiQiVFSbRM4AFpwRBL6MihXFEDxRDYYKinWMJSCwiR4kIuJsLlhM5YkNAcCILOwDwAAIhA8DsDODlLB4SyMFC+mGUWJlFkihEACyEYhhFiUWklIDJSDUdBAo4pU0hQiYsKaAiKGaMFmjIUgTKaKRmgUZo1GaF0ZhohWOJSvAwjBheobGGupwNbMigEoc-QnmMZ5XI1wxXvcGztO1HbvINYxOXVoi2Oe7U9XcawbJWK4BhNq1xD8-xsJ+nCnvNuy3MNbALgNrwTFNHleXepwHf1Y0bpdS0PEgDntu5nmMLdTi3O9zbUYctWvh+xbPcljAmA5ZiHUEi0nRUa1hm80xhOIW1-BsGM7UB+ptbsgNhLC-TQtIJjqlEM4AAp0H8qVld0mjpURWVIDlSB5QVSBFTFlVuc59VI81ep3b1sOXssy1sKNuwXajk24MDs3i1A8NS0gq2Lut8ubcQmO7YtfXtsd6tnWNF1XV9P37WLD1HWMm6S69-0DZ9N2o4tLtjT0YTTSD7Y+0gABEQfHG9AsOmHju4DTfxQzDdtw+uTsjULagTejes49jWODPjnSB51cSx785OU4l5TMa+aeBvn2rI9kNdQtt4bdu2mphDocRV8DDoapoPbaE6FfgkpXrpkhKFoVVGlYlpfHlHhRHhf5kVEdF0k0XR2KkmPz4sWxglIepSCaUg2n8fiqHGWvEnGbFjJySPSC6XZ48qZlSAojPp9z+fC-IFfr5IyUlTLb0FAAdT3v+WEyJgqISZKREqBk4GIVIl0FE0QeKQO5KgxBsVYFIngYgwGRZCHEIor5PBFE-IoKIQgiiAU6EUKCmQ6hoU2H0KZBRFeSAsF-2YQwte5CGHRREdwgh1DSGCKZNRKhXDqK0PEdRJhyjWEyOohwvhGFODKN4co4ReDqJiKMZIhR0iRHoPkfA9BSjUHoIROAnEAA1aB6Z5jUQoqRJkiFgpIgRMzDxXifF+NhJxfhO9kCuOsEE7xvj-G0NiSE-x+kknxNhNItJoSH6eLiaE6KWSkSwiZNOIgFl8ATCAA)

</details>



huangapple
  • 本文由 发表于 2023年7月11日 09:00:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/76658139.html
匿名

发表评论

匿名网友

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

确定