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?
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.
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
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:
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.
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.
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]>;
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; } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With