Is it possible to restrict number to a certain range

TypescriptTypes

Typescript 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];

showing that P has the enumerated types

but instead using functions

const rangeType20 = Array.from(Array(20).keys()) as const;

showing that can't be done with functions

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) Partial solution

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.

Playground Link

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>;

range type

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);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionASDFGerteView Question on Stackoverflow
Solution 1 - TypescriptAdam SzmydView Answer on Stackoverflow
Solution 2 - TypescriptAlexGView Answer on Stackoverflow
Solution 3 - TypescripttitusfxView Answer on Stackoverflow
Solution 4 - TypescriptElie G.View Answer on Stackoverflow
Solution 5 - TypescriptGrafluxeView Answer on Stackoverflow
Solution 6 - TypescriptDean XuView Answer on Stackoverflow
Solution 7 - TypescriptGuillaume MastioView Answer on Stackoverflow
Solution 8 - TypescriptProfesor08View Answer on Stackoverflow
Solution 9 - TypescriptFeirellView Answer on Stackoverflow
Solution 10 - TypescriptudarrrView Answer on Stackoverflow
Solution 11 - TypescriptGibboKView Answer on Stackoverflow
Solution 12 - TypescriptmoqiyuanshiView Answer on Stackoverflow
Solution 13 - TypescriptmosaicView Answer on Stackoverflow