Reputation: 10215
I'm trying to write an external function to narrow the type of objects with TypeScript, however as soon as I pull the logic out TypeScript seems to lose the thread. I've got the following code:
if (element instanceof HTMLInputElement && element.checked) {
// do something
}
However, I'd like to pull the first check out and send us a notification that something is off:
function assertType(element, expectedType) {
if (element instanceof expectedType) { return true; }
notify(`element ${element} wasn't expected type ${expectedType}`);
return false;
}
if (assertType(element, HTMLInputElement) && element.checked) {
// do something
}
TypeScript is happy with the first, but the second it complains about:
error TS2339: Property 'checked' does not exist on type 'HTMLElement'.
Even if I reduce it to the bare minimum it still fails:
function assertType(element, expectedType) {
return element instanceof expectedType;
}
Is there a way I can write the assertType
function so that TypeScript is able to narrow the type? I'm trying to avoid having a type cast to ensure that the types are in line with the underlying html, but I guess I could use that in a pinch. Maybe even something like:
if (assertType(element, HTMLInputElement) && (element as HTMLInputElement).checked) {
// do something
}
Upvotes: 0
Views: 190
Reputation: 328788
This is a job for user-defined type guards, along with generics so that it can apply to any type. Here's one way to make your assertType
function:
function assertType<T>(element: any, expectedType: new(...args: any[])=>T): element is T {
return element instanceof expectedType;
}
The idea is that expectedType
should be a constructor for some generic type T
, and it will return whether or not element is T
. Let's see if it works, type system wise:
declare const element: Element;
if (assertType(element, HTMLInputElement) && (element.checked)) {
// do something
}
No errors. Hope that helps. Good luck!
What if you want to make it even more generic and allow
expectedType
to be either a constructor to match withinstanceof
or a string to match withtypeof
?
That seems a bit much to want one function to do, but if I were to try I'd probably use overloads and lookup types, like this:
type TypeofMapping = {
string: string,
number: number,
boolean: boolean,
symbol: symbol,
undefined: undefined,
object: object,
function: Function
}
function assertType<T>(element: any, expectedType: new (...args: any[]) => T): element is T;
function assertType<K extends keyof TypeofMapping>(element: any, expectedType: K): element is TypeofMapping[K];
function assertType(element: any, expectedType: new (...args: any[]) => any | keyof TypeofMapping): boolean {
return (typeof expectedType === "string") ? typeof element === expectedType : element instanceof expectedType;
}
The point is to have the code pick one of the two overloads. The first one is as before, but the second one constrains the expectedType
to one of the strings that the runtime typeof
operator returns, and maps that string to the type via TypeofMapping
. Let's try it again:
declare const element: Element;
if (assertType(element, HTMLInputElement) && (element.checked)) {
// do something
}
declare const otherThing: string | number;
if (assertType(otherThing, "string") && otherThing.charAt(0) === 'a') {
// do something else
}
Also seems to work (haven't tested at runtime though). Still I'd shy away from that kind of split-personality function myself. Up to you. Good luck again!
Upvotes: 3