TypeScript における HTMLElement の型指定

ついでに、と言っては何だけれどもうひとつ TypeScript のねたを。
正直に言って、TypeScript に触る前まで Javascript において取得した HTML の要素の型など考えたこともなかった。オブジェクトであることはわかっていたけれど。
しかし、TypeScript に関わるつもりならば、そのあたりのこともしっかり頭に入れておかないといけないようだ。
たとえば、よく使うものとしては、こんなのを2つ。

const tarid: HTMLElement | null = document.getElementById( 'target' );

const tarcls: HTMLCollectionOf<Element> = document.getElementsByClassName( 'targets' );
TypeScript
CopyExpand

id と class で要素を取得した場合の違い。単体と複数ということからしてやはり型は違わないといけないのだろう。
そして大枠として HTMLElement があるのだけれど、各タグによってそれぞれに個別の型がある。たとえば、下のように input タグが存在しているとしてその要素を取得する。

// 取得する要素 <input type="hidden" id="elinp" name="elinp" value="test input">

const targetid: HTMLElement | null = document.getElementById( 'elinp' );

if ( null !== targetid ) {

	console.log( targetid.value );// error -> プロパティ 'value' は型 'HTMLElement' に存在しません

	console.log( ( <HTMLInputElement>targetid ).value );// アサーションしてやれば OK!

	console.log( targetid.getAttribute( 'value' ) );// これもOK! error にはならない
}
TypeScript
CopyExpand

input 要素を HTMLElement の型で取得はできるが、”value” というプロパティは持っていないので使えず、”getAttribute” なら持っているから使えるということ。であるなら、この場合、取得するのが inputタグだとわかっているのだから、取得するときの型に ”HTMLInputElement” を指定してやれば済むことではないか、と思うものの “getElementById” の戻り値の型自体が ”HTMLElement | null” のようで、
「型 ‘HTMLElement | null‘ を型 ‘HTMLInputElement | null‘ に割り当てることはできません”。」
と、怒られてしまうことになる。取得するときにアサーションする手もあるけれど、それは先でも後でも同じこと。

*
*
*
*
*

ここでちょっと横道にそれるが、Element.valueElement.getAttribute( ‘value’ ) は挙動が全く同じではないので要注意なのであるが、細かいことをよく忘れてしまうので・・・。たとえば、下のようなことをすると違いが出る。

// <input type="text" id="elinp" name="elinp" value="load">

const targetid = <HTMLInputElement | null>document.getElementById( 'elinp' );

if ( null !== targetid ) {
	// ブラウザのインプットの表示 -> load
	console.log( targetid.value );// -> load

	console.log( targetid.getAttribute( 'value' ) );// -> load

	setTimeout( () => {
		targetid.value = '.val changed';
		// ブラウザのインプット -> .val changed に変わる

		console.log( targetid.value );// -> .val changed

		console.log( targetid.getAttribute( 'value' ) );// -> load

		setTimeout( () => {
			targetid.setAttribute( 'value', 'setAtt changed' );
			// ブラウザのインプット -> .val changed のまま変わらない

			console.log( targetid.value );// -> .val changed
		
			console.log( targetid.getAttribute( 'value' ) );// -> setAtt changed
		}, 1000 );
	}, 3000 );

	// タイムアウト終了後、ブラウザのインプットに form update を入力
	targetid.addEventListener( 'change', () => {
		console.log( targetid.value );// -> form update

		console.log( targetid.getAttribute( 'value' ) );// setAtt changed のまま変わらない
	});
}
TypeScript
CopyExpand

要は、Element.getAttribute( ‘value’ ) で取得した値は、ページロード時の初期値の値であり、その後、”Element.value = ” で値を更新したり、ユーザーがブラウザに直接入力して値を更新しても変更されない。この値が更新されるのは setAttribute( ‘value’ ) で更新した場合だけになる。
一方、Element.value においては、全くその逆で、”Element.value = “で値を更新した場合はもちろん、ユーザーがブラウザに直接入力して値を更新した場合にも更新される。しかし、setAttribute( ‘value’ ) にての値の更新においては、変更されない。そしてこの Element.value の値が、ブラウザの input 要素に表示されている値とシンクロしている。

と、いうことで、通常はブラウザに表示されているフォームとシンクロしている同じ値が必要なはずであるから、Element.value を使うべきで、その要素の初期値が欲しい場合などは、getAttribute( ‘value’ ) で取得できるという感じだと思う。

*
*
*
*
*
*
*
*

HTMLElement の型には ”value” プロパティがないけれど、HTMLInputElement には存在する、ということでちょっと気がつくのは prototype の存在。
ためしに取得した input 要素のオブジェクトを console.log( Object ) で表示させてみると、そのオブジェクトの持っているプロパティがどどっと列挙されて表示される(ただし、これは Firefox の場合、Chrome だと console.dir( Object ) になる)。

表示されるプロパティはたくさんあるが、その中に ”HTMLInputElement”、そして prototype において ”HTMLElement” の文字列を見つけることができる。まぁ、”HTMLElement” において個別の型の文字列がよくわからないときは console を使えばわかるということがわかった。
ちなみに、個別の prototype の名前だけを得たいなら、下で良さそう。これは FirefoxChrome ともに共通。これにて ”HTMLInputElement” の文字列だけが得られる。

const target = document.getElementById( 'target' );

console.log( Object.prototype.toString.call( target ) );
JavaScript
CopyExpand

HTML のタグにおいて共通するプロパティやメソッドなどは、大本の ”HTMLElementprototype” に設定してあって、それぞれのタグに特別なもの、たとえば input タグの “value” のようなものは、それぞれ専用の prototype である ”HTMLInputElement” のようなものが用意されているという認識でいいのではないだろうか。

ところで、はじめのうちは、これの型は何を指定すれば?と、いうことには多く出くわすのだろう。
そんなときは、知りたい型の対象を取得して、console.log( )typeof してみることが一つ。もう一つは、絶対に思ったものとは違うであろう型を指定した変数に、知りたい型の対象を代入してみる。VSCord でエラーがでて、そのエラーの内容で大抵は型がわかるはず。innerHTML で得られるのはただの文字列なのか、はたまたオブジェクトなのかと思ったことがあった。

// <div id="el0" class="elitems">
//	<ul id="elul"><li id="lifirst">one</li><li>two</li><li>three</li></ul>
//	<p id="el0p">paragraph</p>
// </div>

const tarp: HTMLElement = document.getElementById( 'el0' )!;

console.log( typeof tarp.innerHTML );// -> string

// ↓ 意図的にerror にする -> 型 'string' を型 'number[]' に割り当てることはできません
const tar: number[] = tarp.innerHTML;
TypeScript
CopyExpand

それにしてもどうにもしっくりこないのだけれど、「プロパティ ‘value‘ は型 ‘HTMLElement‘ に存在しません。」というエラーは必要なのだろうか。コンパイラにそれと教えてやるということではあるけれど、アサーション自体がこれもごまかしのようなものだろうに。もともとこの部分において Javascript で挙動がおかしくなったりするものでもないだろうに、アサーションでごまかしていいのなら、はじめからすべて HTMLElement でいいではないか。と、思ってしまうのである。

変数によるブラケット記法での css style の変更

たとえば、Javascript において要素の style の設定をする場合、その設定したい style のプロパティが決まっている場合、通常はドット記法によって書くのが簡単である。

document.getElementById( 'target' ).style.marginLeft = 'auto';
JavaScript
CopyExpand

しかし、そのプロパティ名を変数で指定したい場合はこのドット記法では書くことができない。その場合は、[] を使ったブラケット記法で書くことができる。まぁ、なんてことはなく、普通のオブジェクトの扱いとまったく同じである。

const pname = 'marginLeft';

document.getElementById( 'target' ).style[ pname ] = 'auto';
JavaScript
CopyExpand

Javascript において複数の style 設定をまとめて行う場合、Element.style.cssText を使うのが最も高速である。しかし、この cssText が高速なのは、単に ”=” で使った場合であり、その場合、同じ要素に対して設定してある既存のインラインでのスタイル設定( Javascript で設定したスタイルはすべてインラインである)をすべて白紙にして、新しいものだけにしてしまう。その対策として、”+=” で設定すれば白紙にせずに上書きできるのであるけれど、その場合速度的に極端に遅くなってしまい、cssText を使わず個々のプロパティに対して個別に設定したほうが断然速いということになってしまう。

そういう場合に、複数のスタイル設定を連想配列に仕込んでおいて、プロパティ名を変数で指定しつつループさせる場合に、このブラケット記法が必要になってくるというわけだ。そしてその場合、いまのところ(2022/05)hasOwnProperty の条件式を入れた for-in ループが最も高速である。例えばこんな具合に。が、しかしこれは17行目がエラーとなってしまう。なぜか?

const target: HTMLElement | null = document.getElementById( 'target' ),
	style01: { [ key: string ]: string } = { 
		position: 'absolute',
		width: '100%',
		height: '100%',
		top: '0px',
		left: '0px',
		opacity: '0',
		backgroundColor: 'transparent'
	};

if ( null !== target ) {
	for ( const key in style01 ) {
		if ( style01.hasOwnProperty( key ) ) {

			// ↓ error になる -> key : インデックス式が型 'number' ではないため、要素に 'any' 型が暗黙的に指定されます。
			target.style[ key ] = style01[ key ];
		}
	}
}
TypeScript
CopyExpand

エラーを読むと、key においてインデックス式が型 ‘number’ ではないため、とある。なんだそれ!である。これまでの経験からしてこの key は ‘string’ が常識ではないのか?という疑問にぶつかる。「要素に ‘any’ 型が暗黙的に指定されます。」って、一体どの要素が指定されているのだろうか。
この件は、ネットを探せば答えが見つかり、面倒なので詳細は省くが、TypeScript における CSSStyleDeclaration の仕様の都合によるのだそうだ。ということで、対策も2つほど知ることができる。

1つ目は、対象の要素を ‘any’ 型にアサーションしてしまう方法。要素が ‘any’ 型に暗黙的に指定されてしまうのなら、いっそ明示的にこちらから対象を any 型にアサーションしてしまえば良いってことだろうか。確か ‘any’ 型はその要素に対しての型チェックがされないということではなかったか。key が ‘number’ 型ではないというエラーなのに、本体を ‘any’ にしてどうでもよくしてエラーを回避する手法ということか。なんだこのいい加減さは。たしかにエラーは回避できる。そしてこれには、注意書きがある。「It’s not the best solution but it works and is straightforward. 最善の解決法ではないけれど、機能的でわかりやすい。」と。

if ( null !== target ) {
	for ( const key in style01 ) {
		if ( style01.hasOwnProperty( key ) ) {

			( <any>target ).style[ key ] = style01[ key ];// not error
		}
	}
}
TypeScript
CopyExpand

もう一つは、ごまかしではなく正当的な方法。こちらが本当の解答なんだと思う。それはブラケット記法の使用をやめて、Element.style.setProperty を使用する方法に変更すること。

if ( null !== target ) {
	for ( const key in style01 ) {
		if ( style01.hasOwnProperty( key ) ) {
			const value: string | undefined = style01[ key ];

			if ( 'string' === typeof value ) {
				target.style.setProperty( key, value );
			}
		}
	}
}
TypeScript
CopyExpand

まぁ、でも、元となるスタイルの連想配列は固定で変更もなければ、その値は保証できるので、わざわざ条件式を入れて遅くしなくとも、下のようにアサーションしてしまえば良いように思う。

if ( null !== target ) {
	for ( const key in style01 ) {
		if ( style01.hasOwnProperty( key ) ) {

			target.style.setProperty( key, <string>style01[ key ] );
		}
	}
}
TypeScript
CopyExpand

あれっ!結局またアサーションなのか。で、試してみてわかったことだけれど、Element.style.setProperty を使う方法よりもブラケット記法のほうが、わずかながらではあるけれど処理が高速である。


これ、思うのだけれど、Javascript ではブラケット記法に文字列のプロパティ名を指定して当たり前の如くできていたことが、TypeScript の都合によってコンパイルエラーとされてしまうわけだ。Javascript 的には変な値を指定しているとか、変数の型があっていないとかというエラーでもなんでもなくて、あくまで、TypeScript の内部の都合による不具合なわけだ。それゆえ、アサーションというごまかしでもって型を <any> にしてコンパイラをだませば解決する問題という、いい加減というか、なんだかどうでもいいような問題に思えてしまうので、そんな問題なら、単純にアサーションでごまかしてしまえば良いように思うわけで。

これは自分的には、ブラケット記法を any でアサーションして使う方法が、最善の方法だと思えるわけで。

Leave a Reply!

JavaScript is necessary to send a comment.
You can edit and delete your comment if you input a edit key.
Edit key is necessary for attesting you when you edit and delete it.
The tag of HTML cannot be used in comment.
When you comment for the first time, it is displayed after the approval of the administrator.
Because I cannot speak English so much, it takes time to answer.
Required fields are marked *.

※Please enter more than 5 characters only alphabets.
※Edit or delete are possible for 2000 days after approval.

*

♠Simplistic Comment User Editable v4.0

♠When visitors leave comments on the site this site collect the data shown in the comments form, and also the visitor’s IP address and browser user agent string to help spam detection.
♠This site does not use cookie when visitors leave comments and commenter edit comment.
♠This site uses Akismet to reduce spam. Learn how your comment data is processed.

Comments feed

Trackback URL : https://strix.main.jp/wp-trackback.php?p=170788

Sanbanse Funabashi

Top

スクロールさせるか画像をクリックすると元に戻ります。