The main idea is to modify the generic function isNumber()
so that the type T
of parameter param
is limited to being a sub-type of number
. This goes against the usual extends
constraint in TypeScript, which sets an upper bound. Unfortunately, TypeScript doesn't currently provide direct support for lower bounded generics. There is a feature request for this functionality in microsoft/TypeScript#14520, but it might involve using super
instead of extends
, like this:
// INVALID TS (as of 4.7), AVOID THIS
function isNumber<T super number>(param: T): param is number {
return typeof param === "number"
}
Since we can't do this directly, one alternative approach is to simulate lower bounded generics by utilizing a standard upper bound within a conditional type. The expression
T extends (U extends T ? unknown : never)
somewhat mimics the behavior of
T super U
. If
T
is a supertype of
U</code, then <code>(U extends T ? unknown : never)
results in
unknown
, satisfying the constraint (
T extends unknown
). Otherwise, if
T
is not a supertype of
U
, it evaluates to
never
, potentially failing the constraint (
T extends never
).
In this scenario, where T
needs to be either a supertype or subtype of number
, we can use
T extends (number extends T ? unknown : number)
. This way, even if
T
represents a numeric
literal type like
14
, everything should function correctly.
A complication arises because the compiler does not automatically recognize that param
will likely be a supertype of
number</code, preventing the use of the type predicate <code>param is number
. Instead, it's necessary to define a type that satisfies both
T
and
number
, achieved through an
intersection construct like
param is T & number
.
Therefore, a revised version of the isNumber()
function incorporating these changes would look like:
function isNumber<T extends number extends T ? unknown : number>(
param: T
): param is number & T {
return typeof param === "number"
}
Finally, testing confirms that the implementation behaves as expected:
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
Everything seems to be working well.
Link to Playground with Code Example