sno2
sno2

Reputation: 4213

How do I assert a `number` type is an integer?

Let's say I have the following function:

function repeat(times: number, cb: (index: number) => any) {
    for (let i = 0; i < times; i++) {
        cb(i);
    }
}

repeat(3, (i) => console.log(i)); // logs 0 1 2

I want to be able to assert at compile-time that the given number for the number of times to call the callback is an integer. I know that you can do this by taking in a bigint, however, I would rather not allow for numbers to be larger than the regular number limit because the function would most likely not even finish. Is there any way to make TypeScript error when the number provided is not an integer?

Upvotes: 5

Views: 2775

Answers (1)

sno2
sno2

Reputation: 4213

You can do this via some awesome string intrinsic behavior with the number and bigint type. Basically, the ${bigint} template literal type matches any sign (+ / -) optional followed by one or more digits. Now, when you take any number type and interpolate it into a template literal like the following: ${5} it gets normalized into a regular string type like this "5". Therefore, we can take the stringified form of the number and check if it extends the ${bigint} type. If so, then make the parameter type itself. Otherwise, make it never and thus force the user to change their type.

type AssertInteger<N extends number> =
  number extends N ? N : `${N}` extends `${bigint}` ? N : never;

function repeat<N extends number>(times: AssertInteger<N>, cb: (index: number) => any) {
    for (let i = 0; i < times; i++) {
        cb(i);
    }
}

// dummy function
const dummy = (_: number) => {};

// No Errors 🎉
repeat(1, dummy);
repeat(543.0, dummy);
repeat(-43, dummy);
repeat(0, dummy);
repeat(5e5, dummy);
repeat(0.01e3, dummy);

// Correctly errors 🎉
repeat(1.2, dummy);
repeat(-0.9, dummy);
repeat(543.5, dummy);
repeat(-43.8, dummy);
repeat(-43e-1, dummy);

The number extends N ? N clause of the AssertInteger type is just a little check to see if the number type is an exact number type. If it's inexact (number), then we will simply let the integer go through because there would be no way to assert it besides some ugly casts.

TypeScript Playground Link

Upvotes: 4

Related Questions