Reputation: 155558
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
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 beHTMLSelectElement
orHTMLTextAreaElement
which are notHTMLInputElement
but do have thedisabled: 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