Dai
Dai

Reputation: 155558

How do I succinctly assert that an object has a specific property?

I'm converting ASP.NET WebForms' Focus.js (it's embedded in System.Web.dll) into TypeScript (because I'm maintaining a WebForms project that makes heavy-use of ASP.NET WebForms' stock client-side scripts).

Here's the original JavaScript function WebForm_IsInVisibleContainer from Focus.js:

function WebForm_IsInVisibleContainer(ctrl) {
    var current = ctrl;
    while((typeof(current) != "undefined") && (current != null)) {
        if (current.disabled ||
            ( typeof(current.style) != "undefined" &&
            ( ( typeof(current.style.display) != "undefined" &&
                current.style.display == "none") ||
                ( typeof(current.style.visibility) != "undefined" &&
                current.style.visibility == "hidden") ) ) ) {
            return false;
        }
        if (typeof(current.parentNode) != "undefined" &&
                current.parentNode != null &&
                current.parentNode != current &&
                current.parentNode.tagName.toLowerCase() != "body") {
            current = current.parentNode;
        }
        else {
            return true;
        }
    }
    return true;
}

I've annotated it into this TypeScript:

function WebForm_IsInVisibleContainer( ctrl: HTMLElement ): boolean {
    var current = ctrl;
    while( ( typeof ( current ) != "undefined" ) && ( current != null ) ) {
        if( current.disabled ||
            ( typeof ( current.style ) != "undefined" &&
                ( ( typeof ( current.style.display ) != "undefined" &&
                    current.style.display == "none" ) ||
                    ( typeof ( current.style.visibility ) != "undefined" &&
                        current.style.visibility == "hidden" ) ) ) ) {
            return false;
        }
        if( typeof ( current.parentNode ) != "undefined" &&
            current.parentNode != null &&
            current.parentNode != current &&
            (current.parentNode as HTMLElement).tagName.toLowerCase() != "body" ) {
            current = current.parentNode;
        }
        else {
            return true;
        }
    }
    return true;
}

However tsc has two compiler errors:

My quick-fix is to add these type-assertions (below), however this feels like I'm doing something wrong. Especially because ctrl could be HTMLSelectElement or HTMLTextAreaElement which are not HTMLInputElement but do have the disabled: boolean property:

function WebForm_IsInVisibleContainer( ctrl: HTMLElement ): boolean {
    var current = ctrl;
    while( ( typeof ( current ) != "undefined" ) && ( current != null ) ) {
        if( ( current as HTMLInputElement ).disabled ||                                // <-- here
            ( typeof ( current.style ) != "undefined" &&
                ( ( typeof ( current.style.display ) != "undefined" &&
                    current.style.display == "none" ) ||
                    ( typeof ( current.style.visibility ) != "undefined" &&
                        current.style.visibility == "hidden" ) ) ) ) {
            return false;
        }
        if( typeof ( current.parentNode ) != "undefined" &&
            current.parentNode != null &&
            current.parentNode != current &&
            (current.parentNode as HTMLElement).tagName.toLowerCase() != "body" ) {
            current = current.parentNode as HTMLElement;                                // <-- and here
        }
        else {
            return true;
        }
    }
    return true;
}

In JavaScript, it's perfectly fine to use if( current.disabled ) to check for the existence of a property - why can't TypeScript support this?

I know another workaround is to add a new interface like so:

interface DisableableHTMLElement extends HTMLElement {
    disabled: boolean;
}

function WebForm_IsInVisibleContainer( ctrl: DisableableHTMLElement ): boolean {
    // etc
}

...but this feels worse, and also isn't succint.

So how can I do something like this in TypeScript:

function doSomething( foo: Element ): string {
    if( 'type' in foo ) {
        return foo.type as string;
    }
}

(When I do use the above doSomething code, TypeScript says the type of foo inside the if() block is actually never instead of Element).

Upvotes: 2

Views: 1119

Answers (1)

CertainPerformance
CertainPerformance

Reputation: 371049

If current, if it may have a property disabled, will be one of HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, then I think the best thing to do would be to create a type guard for that:

const isDisableableElement = (current: HTMLElement): current is HTMLInputElement | HTMLSelectElement  | HTMLTextAreaElement => {
    return 'disabled' in current;
};

It's not incredibly concise, but like you said:

My quick-fix is to add these type-assertions (below), however this feels like I'm doing something wrong. Especially because ctrl could be HTMLSelectElement or HTMLTextAreaElement which are not HTMLInputElement but do have the disabled: boolean property

You have to choose between conciseness and type-correctness, and type-correctness is probably a better option, especially since it doesn't add that much more code, is straightforward, and has less chance of confusing future readers.

Then, just check the type guard in the condition before checking the .disabled property:

if (
    (isDisableableElement(current) && current.disabled) ||

It looks like you can also trim down the other checks in the code, if you want. If the ctrl parameter is typed correctly and will always be an HTMLElement, it (and its parents) will always have a style property with display and visibility sub-properties. current will never be undefined or null either, since you're doing those checks at the bottom of the loop already (and a .parentNode won't be undefined anyway - it'll only ever be an element or null). The code should never reach past the end of the loop - either a hidden parent will be found and false will be returned, or the final parent will be found and true will be returned:

function WebForm_IsInVisibleContainer(ctrl: HTMLElement): boolean {
    let current = ctrl;
    while (true) {
        if (
            (isDisableableElement(current) && current.disabled) ||
            current.style.display === 'none' ||
            current.style.visibility === 'hidden'
        ) {
            return false;
        }
        if (current.parentNode !== null &&
            current.parentNode !== current &&
            (current.parentNode as HTMLElement).tagName.toLowerCase() !== 'body'
        ) {
            current = current.parentNode as HTMLElement;
        } else {
            return true;
        }
    }
}

Upvotes: 2

Related Questions