diff --git a/index.d.ts b/index.d.ts index f49552fe8..70301dae3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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, diff --git a/readme.md b/readme.md index 0a66e4fe8..0a621232a 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/source/int-range.d.ts b/source/int-range.d.ts new file mode 100644 index 000000000..ba55cd6d3 --- /dev/null +++ b/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 = PrivateIntRange; + +/** +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, // The gap between each number, gap = step - 1 + List extends unknown[] = BuildTuple, // The final `List` is `[...StartLengthTuple, ...[number, ...GapLengthTuple], ...[number, ...GapLengthTuple], ... ...]`, so can initialize the `List` with `[...StartLengthTuple]` + EndLengthTuple extends unknown[] = BuildTuple, +> = Gap extends 0 ? + // Handle the case that without `Step` + List['length'] extends End // The result of "List[length] === End" + ? Exclude // All unused elements are `never`, so exclude them + : PrivateIntRange + // Handle the case that with `Step` + : List extends [...(infer U), ...EndLengthTuple] // The result of "List[length] >= End", because the `...BuildTuple` maybe make `List` too long. + ? Exclude + : PrivateIntRange]>; diff --git a/source/internal.d.ts b/source/internal.d.ts index 341e70689..4054af236 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -14,13 +14,15 @@ Infer the length of the given array ``. type TupleLength = T extends {readonly length: infer L} ? L : never; /** -Create a tuple type of the given length ``. +Create a tuple type of the given length `` and fill it with the given type ``. + +If `` is not provided, it will default to `unknown`. @link https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f */ -type BuildTuple = T extends {readonly length: L} +export type BuildTuple = T extends {readonly length: L} ? T - : BuildTuple; + : BuildTuple; /** Create a tuple of length `A` and a tuple composed of two other tuples, diff --git a/test-d/int-range.ts b/test-d/int-range.ts new file mode 100644 index 000000000..a1ce016c4 --- /dev/null +++ b/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(maxNumberTest);