Skip to content

Commit

Permalink
Add IntRange type (#707)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Emiyaaaaa and sindresorhus committed Oct 17, 2023
1 parent 157ed07 commit e5d145d
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 3 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -81,6 +81,7 @@ export type {WritableKeysOf} from './source/writable-keys-of';
export type {HasWritableKeys} from './source/has-writable-keys';
export type {Spread} from './source/spread';
export type {TupleToUnion} from './source/tuple-to-union';
export type {IntRange} from './source/int-range';
export type {IsEqual} from './source/is-equal';
export type {
IsLiteral,
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -169,6 +169,7 @@ Click the type names for complete docs.
- [`Spread`](source/spread.d.ts) - Mimic the type inferred by TypeScript when merging two objects or two arrays/tuples using the spread syntax.
- [`IsEqual`](source/is-equal.d.ts) - Returns a boolean for whether the two given types are equal.
- [`TaggedUnion`](source/tagged-union.d.ts) - Create a union of types that share a common discriminant property.
- [`IntRange`](source/int-range.d.ts) - Generate a union of numbers.

### Type Guard

Expand Down
52 changes: 52 additions & 0 deletions source/int-range.d.ts
@@ -0,0 +1,52 @@
import type {BuildTuple, Subtract} from './internal';

/**
Generate a union of numbers.
The numbers are created from the given `Start` (inclusive) parameter to the given `End` (exclusive) parameter.
You skip over numbers using the `Step` parameter (defaults to `1`). For example, `IntRange<0, 10, 2>` will create a union of `0 | 2 | 4 | 6 | 8`.
Note: `Start` or `End` must smaller than `1000`.
Use-cases:
1. This can be used to define a set of valid input/output values. for example:
```
type Age = IntRange<0, 120>;
type FontSize = IntRange<10, 20>;
type EvenNumber = IntRange<0, 11, 2>; //=> 0 | 2 | 4 | 6 | 8 | 10
```
2. This can be used to define random numbers in a range. For example, `type RandomNumber = IntRange<0, 100>;`
@example
```
import type {IntRange} from 'type-fest';
// Create union type `0 | 1 | ... | 9`
type ZeroToNine = IntRange<0, 10>;
// Create union type `100 | 200 | 300 | ... | 900`
type Hundreds = IntRange<100, 901, 100>;
```
*/
export type IntRange<Start extends number, End extends number, Step extends number = 1> = PrivateIntRange<Start, End, Step>;

/**
The actual implementation of `IntRange`. It's private because it has some arguments that don't need to be exposed.
*/
type PrivateIntRange<
Start extends number,
End extends number,
Step extends number,
Gap extends number = Subtract<Step, 1>, // The gap between each number, gap = step - 1
List extends unknown[] = BuildTuple<Start, never>, // The final `List` is `[...StartLengthTuple, ...[number, ...GapLengthTuple], ...[number, ...GapLengthTuple], ... ...]`, so can initialize the `List` with `[...StartLengthTuple]`
EndLengthTuple extends unknown[] = BuildTuple<End>,
> = Gap extends 0 ?
// Handle the case that without `Step`
List['length'] extends End // The result of "List[length] === End"
? Exclude<List[number], never> // All unused elements are `never`, so exclude them
: PrivateIntRange<Start, End, Step, Gap, [...List, List['length'] ]>
// Handle the case that with `Step`
: List extends [...(infer U), ...EndLengthTuple] // The result of "List[length] >= End", because the `...BuildTuple<Gap, never>` maybe make `List` too long.
? Exclude<List[number], never>
: PrivateIntRange<Start, End, Step, Gap, [...List, List['length'], ...BuildTuple<Gap, never>]>;
8 changes: 5 additions & 3 deletions source/internal.d.ts
Expand Up @@ -14,13 +14,15 @@ Infer the length of the given array `<T>`.
type TupleLength<T extends readonly unknown[]> = T extends {readonly length: infer L} ? L : never;

/**
Create a tuple type of the given length `<L>`.
Create a tuple type of the given length `<L>` and fill it with the given type `<Fill>`.
If `<Fill>` is not provided, it will default to `unknown`.
@link https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f
*/
type BuildTuple<L extends number, T extends readonly unknown[] = []> = T extends {readonly length: L}
export type BuildTuple<L extends number, Fill = unknown, T extends readonly unknown[] = []> = T extends {readonly length: L}
? T
: BuildTuple<L, [...T, unknown]>;
: BuildTuple<L, Fill, [...T, Fill]>;

/**
Create a tuple of length `A` and a tuple composed of two other tuples,
Expand Down
18 changes: 18 additions & 0 deletions test-d/int-range.ts
@@ -0,0 +1,18 @@
import {expectType, expectError, expectAssignable} from 'tsd';
import type {IntRange} from '../source/int-range';

declare const test: IntRange<0, 5>;
expectType<0 | 1 | 2 | 3 | 4>(test);

declare const startTest: IntRange<5, 10>;
expectType<5 | 6 | 7 | 8 | 9>(startTest);

declare const stepTest1: IntRange<10, 20, 2>;
expectType<10 | 12 | 14 | 16 | 18>(stepTest1);

// Test for step > end - start
declare const stepTest2: IntRange<10, 20, 100>;
expectType<10>(stepTest2);

declare const maxNumberTest: IntRange<0, 999>;
expectAssignable<number>(maxNumberTest);

0 comments on commit e5d145d

Please sign in to comment.