I want to define a function like:
function isNumber(param: ???): param is number {
}
If the param may be a number, e.g. number | string
, number | boolean
, it can be passed to the function. If not, e.g. its type is string
or boolean
, there will be compilation errors.
Is it possible to define such a type?
CodePudding user response:
Conceptually you want to make isNumber()
generic in the type T
of param
, but you want T
to be lower bounded by number
. This is the opposite of the normal extends
constraint which is considered an upper bound. Unfortunately TypeScript does not support lower bounded generics directly. This is requested in microsoft/TypeScript#14520, and if it's ever implemented it might use super
instead of extends
and look like
// NOT VALID TS (as of 4.7), DON'T DO THIS
function isNumber<T super number>(param: T): param is number {
return typeof param === "number"
}
But, as I said, we can't do this.
Instead we can kind of emulate lower bounded generics by using a regular upper bound on a conditional type. Something like T extends (U extends T ? unknown : never)
will behave somewhat like T super U
. If T
is a supertype of U
, then (U extends T ? unknown : never)
evaluates to unknown
, and so the constraint (T extends unknown
) is met. Otherwise, (U extends T ? unknown : never)
evaluates to never
, and so the constraint (T extends never
) is probably not met (unless T
is never
itself, which cannot be guarded against this way).
In your case we are actually fine if T
is either a supertype or a subtype of number
, so we can write T extends (number extends T ? unknown : number)
and that way if T
happens to be a numeric literal type like 14
everything still works.
A wrinkle is that the compiler doesn't understand that param
is likely to be a supertype of number
, so you can't return the type predicate param is number
. Instead you need to write a type that the compiler sees as assignable to T
(and number
). So we'll use the intersection param is T & number
.
So here it is:
function isNumber<T extends number extends T ? unknown : number>(
param: T
): param is number & T {
return typeof param === "number"
}
And let's test it:
isNumber("oops"); // error, Argument of type 'string' is
// not assignable to parameter of type 'number'.
isNumber(Math.random() < 0.5 ? "abc" : 123); // okay
isNumber(14); // okay
Looks good.