Home > other >  How to define a function which only accepts parameters which may be a type in typescript?
How to define a function which only accepts parameters which may be a type in typescript?

Time:07-22

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.

Playground link to code

  • Related