Is it possible to restrict number to a certain range
TypescriptTypesTypescript Problem Overview
Since typescript 2.0 RC (or even beta?) it is possible to use number literal types, as in type t = 1 | 2;
. Is it possible to restrict a type to a number range, e.g. 0-255, without writing out 256 numbers in the type?
In my case, a library accepts color values for a palette from 0-255, and I'd prefer to only name a few but restrict it to 0-255:
const enum paletteColor {
someColor = 25,
someOtherColor = 133
}
declare function libraryFunc(color: paletteColor | 0-255); //would need to use 0|1|2|...
Typescript Solutions
Solution 1 - Typescript
If You have small range, you can always write something like:
type MyRange = 5|6|7|8|9|10
let myVar:MyRange = 4; // oops, error :)
Of course it works just for integers and is ugly as hell :)
Solution 2 - Typescript
No it's not possible. That kind of precise type constraint is not available in typescript (yet?)
Only runtime checks/assertions can achieve that :(
Solution 3 - Typescript
Yes, it's possible BUT:
The 1st. Solution Will be a dirty Solution The 2nd. Solution Will be partial (from x to y where y is a small number, 43 in my case) The 3rd. Solution will be a Complete solution but really advance with Transformers, Decorators, etc.
1. Dirty solution ( the easiest and fast way first ) using @Adam-Szmyd solution:
type RangeType = 1 | 2 | 3
if you need an extensive range, just print and copy/paste:
// Easiest just incremental let range = (max) => Array.from(Array(max).keys()).join(" | "); console.log('Incremental') console.log(range(20)) // With range and steps let rangeS = (( min, max, step) => Array.from( new Array( max > min ? Math.ceil((max - min)/step) : Math.ceil((min - max)/step) ), ( x, i ) => max > min ? istep + min : min - istep ).join(" | ")); console.log('With range and steps') console.log(rangeS(3,10,2))
You may be tented of doing things like this
const data = [1, 2, 4, 5, 6, 7] as const;
type P = typeof data[number];
but instead using functions
const rangeType20 = Array.from(Array(20).keys()) as const;
But at the moment this doesn't work, only work if is a literal. Even the error is not quite correct.
2. Partial solution (source)
type PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T ? ((t: T, ...a: A) => void) extends ((...x: infer X) => void) ? X : never : never;
type EnumerateInternal<A extends Array<unknown>, N extends number> = { 0: A, 1: EnumerateInternal<PrependNextNum<A>, N> }[N extends A['length'] ? 0 : 1];
export type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[] ? E : never;
export type Range<FROM extends number, TO extends number> = Exclude<Enumerate<TO>, Enumerate<FROM>>;
type E1 = Enumerate<43>;
type E2 = Enumerate<10>;
type R1 = Range<0, 5>;
type R2 = Range<0, 43>;
3. Complete solution but really advance with Transformers
, Decorators
, etc.
Using the functions on the first solution, you could replace at compiletime
by the values, using transformer. Similarly, but on runtime
using decorators.
Solution 4 - Typescript
It's not possible for the moment but there's an open issue on GitHub. Currently they are still waiting for a proposal but this functionality might come someday.
In short you won't be able to use a range of numbers as a type until a proposal comes out.
Update - August 2021
A proposal exists now. For more details, see Interval Types / Inequality Types.
Solution 5 - Typescript
While not the best solution (since some checking will be handled at runtime), it's worth mentioning that "opaque types" can help enforce that you're inputting the expected values.
Here's a example:
type RGBColor = number & {_type_: "RGBColor"};
const rgb = (value: number): RGBColor => {
if (value < 0 || value > 255) {
throw new Error(`The value ${value} is not a valid color`);
}
return value as RGBColor;
};
// Compiler errors
const color1: RGBColor = 200; // fail - number is not RGBColor
const color2: RGBColor = 300; // fail - number is not RGBColor
// Runtime error
const color3: RGBColor = rgb(300); // fail - The value 300 is not a valid color
// Pass
const color4: RGBColor = rgb(100);
const color5: RGBColor = rgb(255);
Solution 6 - Typescript
Update 1
Since typescript v4.5 add tail recursive evaluation of conditional types
. Issue Link
Now the maximum number can be 998. It's totally enough for your question.
type Ran<T extends number> = number extends T ? number :_Range<T, []>;
type _Range<T extends number, R extends unknown[]> = R['length'] extends T ? R[number] : _Range<T, [R['length'], ...R]>;
type R5 = Ran<998>
const a: R5 = 3 // correct
const b: R5 = 999 // wrong
Origin Answer
It is possible now with Typescript 4.1 Recursive Conditional Types
type Range<T extends number> = number extends T ? number :_Range<T, []>;
type _Range<T extends number, R extends unknown[]> = R['length'] extends T ? R['length'] : R['length'] | _Range<T, [T, ...R]>;
type R5 = Range<5>
const a: R5 = 3 // correct
const b: R5 = 8 // error. TS2322: Type '8' is not assignable to type '0 | 1 | 2 | 3 | 4 | 5'.
But unfortunately, if your length is too long, the recursive type will fail
type R23 = Range<23>
// TS2589: Type instantiation is excessively deep and possibly infinite.
Well, it works but not really works. :)
Solution 7 - Typescript
It is possible with Typescript 4.5 to do a tail-recursion elimination on conditional types.
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>
type Range<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>
type T = Range<20, 100>
Solution 8 - Typescript
> Is it possible to restrict a type to a number range, e.g. 0-255, without writing out 256 numbers in the type?
Not posible until now, but you can make a lifehack, and generate desired sequence with one line of code and copy/paste result
new Array(256).fill(0).map((_, i) => i).join(" | ")
Solution 9 - Typescript
EDIT: Ahh I did not read the provided answers carefully enough! @titusfx already provided this answer in another form. As with his approach this is limited in respect to the amount of numbers you can generate. This is not an actual solution but a workaround which works in a very limited range of numbers!
Original answer:
There is a workaround to this. Borrowing from the answer https://stackoverflow.com/a/52490977 (which limits this solution to TypeScript v 4.1 and higher):
type _NumbersFrom0ToN<
Nr extends number
> =
Nr extends Nr ?
number extends Nr ?
number :
Nr extends 0 ?
never :
_NumbersFrom0ToNRec<Nr, [], 0> :
never;
type _NumbersFrom0ToNRec<
Nr extends number,
Counter extends any[],
Accumulator extends number
> =
Counter['length'] extends Nr ?
Accumulator :
_NumbersFrom0ToNRec<Nr, [any, ...Counter], Accumulator | Counter['length']>;
type NrRange<
Start extends number,
End extends number
> =
Exclude<_NumbersFrom0ToN<End>, _NumbersFrom0ToN<Start>>;
let nrRange: NrRange<14, 20>;
Which creates the type 14 | 15 | 16 | 17 | 18 | 19
. To make this work we just need to leverage the feature that TypeScript can count via the length attribute of the new improved tuple type inspections. So we just extend an array as long as the length of the array is not the same as the input number. While we extend the array we remember the lengths which we already visited. This in turn results in a counter with extra steps.
EDIT: I put those types in a package for an easy reausability: https://www.npmjs.com/package/ts-number-range
Solution 10 - Typescript
With validation range numbers (positive and integer range) ts 4.6.3
type IsPositive<N extends number> = `${N}` extends `-${string}` ? false : true;
type IsInteger<N extends number> = `${N}` extends `${string}.${string}`
? never
: `${N}` extends `-${string}.${string}`
? never
: number;
type IsValid<N extends number> = IsPositive<N> extends true
? IsInteger<N> extends number
? number
: never
: never;
type PositiveNumber<
N extends number,
T extends number[] = []
> = T["length"] extends N ? T[number] : PositiveNumber<N, [...T, T["length"]]>;
type Range<N1 extends IsValid<N1>, N2 extends IsValid<N2>> = Exclude<
PositiveNumber<N2>,
PositiveNumber<N1>
>;
type RangeType = Range<1, 5>;
And here with negative range but with some constraints. Range are literals. I don't why but i couldn't to get not literaly negative numbers. Maybe someone knows warkaround
type IsInteger<N extends number> = `${N}` extends `${string}.${string}`
? never
: `${N}` extends `-${string}.${string}`
? never
: number;
type NegativeLiteralNumbers<
N extends number,
T extends string[] = []
> = `${N}` extends `-${string}`
? `-${T["length"]}` extends `${N}`
? T[number]
: NegativeLiteralNumbers<N, [...T, `-${T["length"]}`]>
: never;
type PositiveLiteralNumber<
N extends number,
T extends string[] = []
> = `${N}` extends `${string}`
? T["length"] extends N
? T[number]
: PositiveLiteralNumber<N, [...T, `${T["length"]}`]>
: never;
type RangeLiteralNegative<F extends number, T extends number> = Exclude<
NegativeLiteralNumbers<F>,
NegativeLiteralNumbers<T>
>;
type RangeLiteralPositive<F extends number, T extends number> = Exclude<
PositiveLiteralNumber<T>,
PositiveLiteralNumber<F>
>;
type RangeLiteral<N1 extends IsInteger<N1>, N2 extends IsInteger<N2>> =
| (`${N1}` extends `-${string}`
? RangeLiteralNegative<N1, 0>
: `${N1}` extends `${string}`
? RangeLiteralPositive<0, N1>
: never)
| (`${N2}` extends `-${string}`
? RangeLiteralNegative<N2, 0>
: `${N2}` extends `${string}`
? RangeLiteralPositive<0, N2>
: never);
type RangeLiteralType = RangeLiteral<-5, 3>;
Solution 11 - Typescript
Not using static type-checking, only at runtime for example using a library like io-ts
where you could use taggedUnion
for instance:
https://github.com/gcanti/io-ts/issues/313
Solution 12 - Typescript
my solution with tail recursion
type BuildArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = []
> = Arr['length'] extends Length
? Arr
: BuildArray<Length, Ele, [...Arr, Ele]>;
type Add<Num1 extends number, Num2 extends number> = [...BuildArray<Num1>,...BuildArray<Num2>]['length'];
type Subtract<Num1 extends number, Num2 extends number> = BuildArray<Num1> extends [...arr1: BuildArray<Num2>, ...arr2: infer Rest] ? Rest['length'] : never;
type _RangeOf<start extends number, end extends number, R extends unknown[] = [start]> = R['length'] extends Subtract<end, start> ? [...R, end][number] : _RangeOf<start, end, [...R, Add<start, R['length']> ]> ;
type myRange = _RangeOf<2, 7>; // 2, 3, 4, 5, 6, 7
const myRange: myRange = 7;
const myRange2: myRange = 1; // error
const myRange2: myRange = 8; // error
Solution 13 - Typescript
This worked for me, to constrain the height of an html textarea. It clips a test value to the range 5...20.
const rows = Math.min(Math.max(stringArray.length, 5), 20);