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 constructor
s 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); } }
Advertisement
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?