網站搜尋

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> = Exclude<Enumerate<T>, Enumerate<F>>

這適用於整數範圍,例如 IntRange<25, 200>,但是當我給它一個負數,例如 IntRange<-25, 200> 時,我就會收到這個錯誤:

TS2589: Type instantiation is excessively deep and possibly infinite.

我如何在 TypeScript 中支援帶有負數的整數範圍?

答案 #1

TypeScript 原生不支援 數字範圍類型;目前關於這個功能的公開討論為 microsoft/TypeScript#54925,尚不清楚這是否會被實現。在此之前,我們需要使用數字字面量類型的聯合來表示整數範圍。

此外,TypeScript 原生不支持 對數字字面量類型進行數學運算;目前針對此功能的開放請求是 microsoft/TypeScript#26382。在此功能實現之前,我們將需要使用迂迴的方法來讓型別系統計算這些東西。

我知道的讓型別系統對數值字面量型別進行類數學運算的兩種方法是:

  • 操作元組類型並檢查其長度屬性。 例如,對元組類型添加或前置元素對應於遞增,而串接元組類型則對應於加法。 這相當直接,但限制了定義域的非負整數,且大小相當適中。 你的列舉效用類型就是這樣運作的。

  • 模板字面量類型的操作以及對解析後字串字面量類型的更精確推斷。這讓你可以將字串字面量類型轉換為數字字面量類型,反之亦然,因此你可以設計基於字串的數學運算實現,透過序列化和反序列化 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

所以這對你問的問題來說是可行的。 請注意,列舉是一種尾遞迴條件型,因此它必然只適用於 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}${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]

這些是條件型別,其中 Inc 對負數會解析為 Dec,反之亦然,且最後一位數會被遞增或遞減,然後在必要時對前一位數執行進位或借位運算,並遞迴進行。接著 Intrange 可以被簡單地定義為

type IntRange<F extends number, T extends number, A extends number = never> =
  F extends T ? A : IntRange<Inc<F>, T, F | A>

在這裡你只需要從 F 增加到 T。讓我們測試一下:

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

這對相對較大的數字也適用,儘管對於以科學記數法書寫的數字、分數等,它會徹底失敗。


處理任意大的數字需要更多的工作。同樣地,處理範圍中 T 減去 F 大於一千但小於大約十萬的情況也需要更多工作。處理 T 減去 F 大於十萬的範圍則需要對數值範圍類型的本地支援,因為聯集不能容納多於大約十萬個元素。

程式碼的 Playground 連結