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?
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:
- Limit literal arguments of a function.
- 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;
}
}
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 negate(4/2)
or negate(1+1)
doesn't work –
Revert number
instead of a subset of number, namely the union of number literals. –
Dryclean ${T}
extends -${string}
| ${string}.${string}
? never : T; –
Distract type NonNegativeInteger<T extends number> = `${T}` extends `-${string}` | `${string}.${string}` ? never : T;
–
Osuna 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.
@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.
© 2022 - 2024 — McMap. All rights reserved.