const intSymbol = Symbol('integer')
type integer = number & {[intSymbol]: never}
const isInteger = (n: unknown): n is integer => Number.isInteger(n)
function f(s: string | number) {
if (isInteger(s)) {
const allowed = s.toExponential()
} else {
// s still string | number
}
}
With this you even get to define functions that must accept integers, which is kinda neat.
How does it not address your concern? It’s quite literally making isInteger into a type guard. The ‘integer’ type above can passed to any place ‘number’ is needed.
What I was showing here is that this solution is simpler than yours and just as good. Rather than add a utility function and a faux primitive type, I just do the normal workaround for TypeScript not supporting this, which is to redundantly check that something is both a number and an integer.
The point of having a utility function is that you don't have to do the redundant checks every time you want to make sure it's both a number and an integer.
The critical bit is that you need to define a new type for `integer`s distinct from `number`s to allow reusing the code in a way that doesn't break the type system on the negative path, as Ryan and I demonstrated.
You asked for isInteger to work as a type guard and I showed you how. If you prefer explicit casts everywhere that's fine, but it isn't a type guard. You could even use the type anonymously if you really want: (n is number & {Symbol(): never}).