Generic type guard including primitives

I’m trying to create a generic type guard, I’ve read answers that led me to this solution:

function typeGuard<T>(o, constructor: { new(...args: any[]): T }): o is T {
    return o instanceof constructor;
}

This works for any class that has a constructor, e.g.:

class b {
    k: number;

    constructor(k: number) {
        this.k = k;
    }
}
console.log(typeGuard(new b(5), b));

I’m having trouble getting this to work for something like:

console.log(typeGuard(5, number));

or

console.log(typeGuard<number>(5));

I’ve tried this:

type prim = "string" | "number" | "boolean"; // or without quotes
function typeGuard<T>(o, constructor: { new(...args: any[]): T }): o is T;
function typeGuard<T extends prim>(o): o is T;
function typeGuard<T>(o, constructor?): o is T {
    if (constructor) {
        return o instanceof constructor;
    }
return typeof o ==="string";
}

But this implementation doesn’t let me look into what T is and do something like if typeof o === T or something like that.

Is there a way to implement this? Theoretically I’d like to pass string as the constructor argument like typeGuard(5, string) but this would require constructors type to be: { new(...args: any[]): T } | Type<string> | Type<number> | Type<boolean> but I don’t know how to implement this in typescript.

Example of use:

class firstClass {
    n: number;
    constructor(n: number) {
        this.n = n;
    }
}

class secondClass {
    id: Date;
    constructor(d: Date) {
        this.id = d;
    }
}

function toConsole(a): void {
    if (typeGuard(a, firstClass)) {
        console.log(a.n);
    } else if (typeGuard(a, secondClass)) {
        console.log(a.id);
    } else if (typeGuard(a, string)) {
        console.log(a);
    }
}

Answer

I’m still not sure what the real need is for this to be a single function, but let’s see what we can do. You need to provide, at runtime, a value for the function to use to determine if you’re checking for a string, number, or something else.

Let’s say that the second argument to typeGuard() is called sentinel, of type Sentinel, which can either be a constructor, or one of the string values corresponding to what typeof gives you.

type TypeofMap = {
  string: string,
  number: number,
  boolean: boolean
}
type Sentinel = (new (...args: any[]) => any) | keyof TypeofMap;

Then, given a value of a type that extends Sentinel, the type you’re guarding is related to the type of Sentinel via the following conditional type:

type GuardedType<T extends Sentinel> = T extends new (...args: any[]) => infer U ? 
  U : T extends keyof TypeofMap ? TypeofMap[T] : never;

And you can implement typeGuard() like this:

function typeGuard<T extends Sentinel>(value: any, sentinel: T): value is GuardedType<T> {
  // assign to Sentinel instead of generic T to allow type guarding†
  const concreteSentinel: Sentinel = sentinel;
  if (typeof concreteSentinel === "string") {
    return typeof value === concreteSentinel;
  } else {
    return value instanceof concreteSentinel;
  }
}

(† See Microsoft/TypeScript#13995 for the reason for concreteSentinel)

And here’s how you’d use it:

declare const thing: string | number | RegExp;
if (typeGuard(thing, "string")) {
  console.log(thing.charAt(0));
} else if (typeGuard(thing, RegExp)) {
  console.log(thing.flags);
} else {
  console.log(thing.toFixed(0));
}

Does that make sense?

Leave a Reply

Your email address will not be published. Required fields are marked *