英文:
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<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>>
This works for integer ranges like IntRange<25, 200>
but when I give it a negative like IntRange<-25, 200>
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>
:要么F
和T
都是负数,都是非负数,要么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
是一种尾递归条件类型,因此它仅适用于T
和F
的数量小于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's not clear that this will ever be implemented. Until and unless this happens, we'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'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<F, T>` to work with negative numbers, therefore, you'll need to use template literals types, at least in part. Here's a way to negate a number:
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
That works by parsing the string representation of a number and either peeling off or adding on an initial `"-"` character.
You can use that with `Enumerate` as follows:
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>;
There are three valid ways to call `IntRange<F, T>`: 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're in, and then delegate either to `NonNegativeIntRange` (your original implemenation), `NegativeIntRange` (change `F` to `Negate<T>` and `T` to `Negate<F>`, call `NonNegativeIntRange<F, T>`, and then `Negate` the result), or `MixedIntRange` (use `Enumerate` on `Negate<F>` and `T` and fiddle with endpoints).
Let's test it:
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
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't need situations where `T` minus `F` is bigger than 1000 (or 999 or something) then you'll need to abandon `Enumerate`'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<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}${Digit}` ?
`${T}` extends `${F}${infer D extends Digit}` ?
`${D extends 9 ? Inc<F> : 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<T extends number> =
`${T}` extends `-${infer N extends number}` ? `-${Inc<N>}` 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<F> extends 0 ? "" : Dec<F> : 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<F extends number, T extends number, A extends number = never> =
F extends T ? A : IntRange<Inc<F>, T, F | A>
where you just increment from `F` to `T`. Let's test it:
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
type W = IntRange<12345678, 12345703>
// 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<-987654321, -987654315>
// 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>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论