Generic TypeScript type guards with only generic type parameter

1.4k Views Asked by At

I want to build a generic type guard in TypeScript with the following signature:

declare function typeGuard<T>(obj: any): o is T;

I found some articles (e.g. this) and these solved it with a signature like this:

declare function typeGuard<T>(obj: any, clazz: T): o is T;

But this requires one to carry around the type information through the code, so the first signature would be preferred.

The solution in the second case looks as follows:

type typeMap = {
  string: string;
  number: number;
  boolean: boolean;
}

type PrimitiveOrConstructor =
  | string
  | { new (...args: any[]): any }
  | keyof typeMap;

type GuardedType<T extends PrimitiveOrConstructor> = T extends { new(...args: any[]): infer U; } ? U : T extends keyof typeMap ? typeMap[T] : never;

function typeGuard<T extends PrimitiveOrConstructor>(o: any, className: T): o is GuardedType<T> {
  const localPrimitiveOrConstructor: PrimitiveOrConstructor = className;
  if (typeof localPrimitiveOrConstructor === 'string') {
    return typeof o === localPrimitiveOrConstructor;
  }
  return o == localPrimitiveOrConstructor || (typeof localPrimitiveOrConstructor === 'object' && o instanceof localPrimitiveOrConstructor);
}

Which results in:

typeGuard(2, 'number'); // true
typeGuard('foobar', 'string'); // true
typeGuard(new Foobar(), Foobar); // true

But what if I'm in a generic type context e.g. a function like this:

declare function <T>func(arg: T);

In this case, it is not possible to do:

function <T>func(arg: any) {
    if (typeGuard(arg, T)) { ... } // throws error: T is used as a value

So in this case I would prefer something like this:

function <T>func(arg: any) {
    if (typeGuard<T>(arg)) { ... }

I recently have read this article and tried to come up with something like this (which is of course not working):

type Check<X, Y> = X extends Y ? true : false;
declare function check<C, T>(o: T): Check<T, C>;

and then use it like this:

function <T>func(arg: any) {
    if (check<T>(arg)) { ... }

PS: just to be sure, I do not want to change the signature of the wrapping function to something like this function <T>func(arg: any, clazz: T) so that I can clazz in again.

1

There are 1 best solutions below

5
On BEST ANSWER

You can't write a fully generic type guard in typescript, because there's no type information at runtime. At runtime your functions can't know anything about T. See how the code of your function would be transpiled in the typescript playground.

Whenever T would reach JS, it means that you use T as a value, not a type, and you get that error you mentioned: 'T' only refers to a type, but is being used as a value here.,

And the resulting JS code is not type safe.


Let's take a look at what happens with currying. We start from the two parameter function you provided, but I'll swap the parameters, to get to the type guard

function typeGuard<T extends PrimitiveOrConstructor>(className: T, o: any): o is GuardedType<T>

The curried version of this function is:

function curriedTypeGuard<T extends PrimitiveOrConstructor>(className: T): (o: any) => o is GuardedType<T>

This acts like a factory for more specialized single parameter type guards.

const stringSpecializedTypeGuard = curriedTypeGuard("string") // (o: any) => o is string
const classSpecializedTypeGuard = curriedTypeGuard(Class); // (o: any) => o is Class

The types are restricted by specifying the first parameter. Link to playground here

So even with currying you can't implement your func.