Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to represent a non-negative integer in TypeScript so that the compiler would prevent using fractions and negatives?

Tags:

The number is a very loose representation which sometimes is required to be tighten. In my case I wish a variable was only able to take non-negative integers. Is there a way to enforce this constraint in TypeScript?

like image 630
Trident D'Gao Avatar asked Jan 20 '14 01:01

Trident D'Gao


People also ask

How do you display negative numbers in JavaScript?

To use negative numbers, just place a minus (-) character before the number we want to turn into a negative value: let temperature = -42; What we've seen in this section makes up the bulk of how we will actually use numbers.


1 Answers

Update 2021:

Yes, template literals allow this to be done; observe:

type NonNegativeInteger<T extends number> =     number extends T          ? never          : `${T}` extends `-${string}` | `${string}.${string}`             ? never              : T; 

Note that number extends T is necessary to restrict a general number type.

Usage:

function negate<N extends number>(n: NonNegativeInteger<N>): number {     return -n; }   negate(3); // success negate(3.1); // failure negate(-3); // failure negate(-3.1); // failure 

Usage

In response to @ianstarz comment:

How would you use this on a class field or variable type? myField: NonNegativeInteger = 42 doesn't seem to work—I'm not sure what to pass in as the generic type in this case. Can you also provide an example of how to use the generic in this case?

Understand that in Typescript, literals are considered types unto themselves; example: 10 is assignable to some let x: number, but only 10 is assignable to some let x: 10. Furthermore, Typescript's has a powerful type-inference system, but it can only go so far before becoming a burden to develop with. The goal of the above type is to do one of two things:

  1. Limit literal arguments of a function.
  2. Apply further type manipulation.

Your question doesn't just apply to class fields, nor the above type. Typescript variables apply type inference at the time of declaration, not assignment; this inference does not extend to Generics on variables.

To demonstrate the difference between generic variable types and function calls, consider the error below when using a generic identity type:

type Identity<T> = Identity;  // Generic type 'Example' requires 1 type argument(s) let x: Identity = 10; 

Compared to:

type Identity<T> = Identity;  function identity<T>(x: Identity<T>): T {     return x; }  let y = identity(10); // Success, y has type `number` const z = identity(10); // Success, z has type `10` 

Note how z has assumed a literal type. In fact, we could explicitly type y the same, but it would only allow 10 as a value, not any other number.

Finite Literal Unions

If you had a finite amount of integer values, like file descriptors, make a field with a type like the following:

type EvenDigit = 0 | 2 | 4 | 6 | 8;  let x: EvenDigit = 2; // Success let y: EvenDigit = 10; // Failure 

If you're crazy, write a script that generates the union types. Note there is likely a version specific cap on the amount of members for a union type.

Calculated Literal Union

If you wanted to go SUPER meta something like this would generate a range of types:

// Assumes, for simplicity, that arguments Start and End are integers, and // 0 < Start < End. // Examples: // Range<0, 5> -> 0 | 1 | 2 | 3 | 4 | 5 // Only can calculate so much: // Range<0, 100> -> 'Type instantiation is excessively deep and possibly infinite.ts(2589)' // Tail end recursion being introduced in Typescript 4.5 may improve this. type Range<Start extends number, End extends number> = RangeImpl<Start, End>; type RangeImpl<     Start extends number,     End extends number,     T extends void[] = Tuple<void, Start> > = End extends T["length"]     ? End     : T["length"] | RangeImpl<Start, End, [void, ...T]>;  // Helper type for creating `N` length tuples. Assumes `N` is an integer // greater than `0`. Example: // Tuple<number, 2 | 4> -> [number, number] | [number, number, number, number] type Tuple<T, N extends number> = TupleImpl<T, N>; // prettier-ignore type TupleImpl<T, N extends number, U extends T[] = []> =     N extends U["length"]         ? U         : TupleImpl<T, N, [T, ...U]>;  

Generic Assignment Method

You can create a class with assignment and retriever methods (not a getter/setter pair because An accessor cannot have type parameters ts(1094) ).

Example:

class MyClass {     private _n: number = 42;          // infers return type `number`     getN() {         return this._n;     }      setN<T>(n: NonNegativeInteger<T>) {         // Optionally error check:         if (Number.isInteger(n) || n <= 0) {             throw new Error();         }         this._n = value;     } } 
like image 57
Hunter Kohler Avatar answered Sep 29 '22 17:09

Hunter Kohler