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

3

46

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?

Crake answered 20/1, 2014 at 1:30 Comment(0)
D
58

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;
    }
}
Dryclean answered 2/10, 2021 at 0:23 Comment(8)
Wowie zowie, nice typing! I look forward to recognizing your name on the news someday!Deedee
How would you use this on a class field or variable type? myField: NonNegativeInteger<number> = 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?Commination
this only support literal typed values : negate(4/2) or negate(1+1) doesn't workRevert
@Yukulélé That's because those expressions resolve to type 'number'.Dryclean
I don't believe the "number extends T ? never : ..." is necessary as your are specifying it in the declaration <T extends number>.Yearbook
@Yearbook I'm trying to make an error if the type is actually number instead of a subset of number, namely the union of number literals.Dryclean
The note from @Yearbook is correct. I tried implementing this and kept running into the error Argument of type 'number' is not assignable to parameter of type 'never' . Going to this worked for me: type NonNegativeInteger<T extends number> = ${T} extends -${string} | ${string}.${string} ? never : T;Distract
@Distract Thank you, that worked for me too. Here it is again with the backticks escaped with backslashes: type NonNegativeInteger<T extends number> = `${T}` extends `-${string}` | `${string}.${string}` ? never : T;Osuna
I
11

No, this is not possible; there is no* uint or similar in JavaScript, so no corresponding type in TypeScript. There is a open feature request for Contracts which would allow you to provide more robust assertions like these, if it is ever implemented.

* Such data types exist in the Typed Array specification, but these are extensions designed primarily for WebGL, not part of the core language.

Impeachable answered 20/1, 2014 at 3:4 Comment(1)
The Contracts feature request was closed in 2020 ;c. Though there is a fresh similar one: Proposal: Interval TypesLuralurch
T
2

@CSnover's answer is now outdated as the feature request has been rejected.

But indeed as CSnover said, it's not possible in TypeScript.

This Babel plugin may be the closest approach you may have.

Thorp answered 23/11, 2020 at 12:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.