Reputation: 486
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);
}
}
Upvotes: 1
Views: 948
Reputation: 329523
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?
Upvotes: 2