Wonderful! WordPress

TypeScript error accessing object using dynamic variable key

作り手が Microsoft ということで、あまり近づく気にならなかった TypeScript だけれど、多く使われているようであるし、まったく知らないのもなんだし、ちょっと触っておいたほうがいいのかもという気がした。
コンパイルしてできるものが完全なる Javascript のコードであり、実際のページ読み込みに関しては、その TypeScript はまったく関与しないということなので、ページ表示の負荷になることはないというところがとても好ましく、それもその気になった一因でもある。

とりあえず、基本的なことを「TS入門 サバイバルTypeScript 」で学んでから、自サイトの既存の Javascript ファイルを TypeScript で書き直すというところから取り掛かる。しかし、あっけなく落とし穴にはまり込んでしまい抜け出せなくなってしまった。いったいどれくらいの時間を費やしてしまったのであろうか?丸一日?いやもっとだろう。なんてことか、こんなことに何十時間も囚われてしまっていたなんて・・・。

noUncheckedIndexedAccess オプションと undefined

と、いうことで本題。
いったいどういうことで落とし穴にはまったかというと、ただ、二次元の配列をループの中で使うということだった。
具体的にいうと、Javascript によって遅らせてロードさせたい画像が二つあり、ウィンドウの幅によってそのロードさせたいファイルをあらかじめ設定しておこうという処理。
ちなみに imgsrc は設定した画像ファイルの url を入れておく配列で、loadimg は画像ファイル名の、サイズの指定部分のない基礎部分が入れてある配列。この場合、どちらもどうでもいいというか関係なし。

window.addEventListener( 'load', () => {

    const sizespecify: string[][] = [ [ '_s', '_760' ], [ '_ss', '_400' ] ];

    for ( let i = 0; i < 2; ++i ) {
        if ( window.innerWidth > 399 ) {
            imgsrc[ i ] = loadimg[ i ] + sizespecify[0][ i ] + '.webp';// error
        } else {
            imgsrc[ i ] = loadimg[ i ] + sizespecify[1][ i ] + '.webp';// error
        }
    }
});
TypeScript
CopyExpand

これにおいて、ループ内の sizespecify[0]sizespecify[1] の下には赤い波線が付けられてエラーであることを示され、「オブジェクトは ‘undefined’ である可能性があります。」と怒られることになる。

この件については、「TS入門 サバイバルTypeScript – noUncheckedIndexedAccess」のページにも書いてある。
noUncheckedIndexedAccess はインデックス型のプロパティや配列要素を参照したとき undefined のチェックを必須にするコンパイラオプションです。」
とのことで、このオプションを true に設定すると、インデックス型や配列で宣言されたオブジェクトが持つプロパティへのアクセスを厳密に評価し、厳密に定義されていないプロパティは undefined型とのユニオン型として解釈されるようになるということ。

そう、これは noUncheckedIndexedAccess オプションを true にすると起きることで、false であればエラーは発生しない。

で、そう言われて無理やり考えてみると、sizespecify[0][ i ] の i に2が入ることを無理強いすれば、それは undefined になってしまうのではあるけれど、ループにおいて i の値は 0 か 1 に限定されているわけだから、いったいどういう具合において undefined の可能性が生じるのか頭を抱え込んでしまったわけである。いや、まてよ、無理強いすれば・・・、って人間がタイプミスすれば・・・!ってことか。

こんなことになんだかずいぶんと難儀してしまったので、イライラがつのってしまったが、そんなどうでもいいことはおいといて、これはシンプルに考えて元の配列を一次元の二つの配列にすればとりあえずエラーは回避できたのではあるけれど、オブジェクトが undefined である可能性は、はたして変わったのだろうか?いやはや、変わらないのではないだろうか。キーの値を変数にしたり数値にしてみたりとあれこれやってみたけれど状況に変化はなく、一次元ならよくて二次元ならダメというのはいったいどういうことなのか、自分のつるつる脳みそでは全くわからない。なんだこれは!いったいどういうこと?

const sizespecifya: string[] = [ '_s', '_760' ],
    sizespecifyb: string[] = [ '_ss', '_400' ];

for ( let i = 0; i < 2; ++i ) {
    if ( window.innerWidth > 399 ) {
        imgsrc[ i ] = loadimg[ i ] + sizespecifya[ i ] + '.webp';// not error
    } else {
        imgsrc[ i ] = loadimg[ i ] + sizespecifyb[ i ] + '.webp';// not error
    }
}
TypeScript
CopyExpand

ちょっともとに戻って、二次元の配列をそのすべての要素にアクセスすべく、二重のループをさせるのにエラーが出ない方法をと、やってみたのが下。もっと簡単に書ける方法があるのかもしれないけれど・・・。うはっー!めんどくせぇっ!

const keys: Array<Array<number>> = [ [ 2, 4, 6, 8, 10 ], [ 1, 3, 5, 7, 9 ] ],
    keys_len: number = keys.length,
    ary: number[] = [];

for ( let i = 0; i < keys_len; ++i ) {

    if ( 'object' === typeof keys[ i ] ) {

        const key: number[] = <Array<number>>keys[ i ],
            key_len: number = key.length;

        for ( let j = 0; j < key_len; ++j ) {

            if ( 'number' === typeof key[ j ] ) {

                const val: number = <number>key[ j ];

                ary[ val ] = val + i + j;
            }
        }
    }
}
TypeScript
CopyExpand

ちなみに先述の「TS入門 サバイバルTypeScript – noUncheckedIndexedAccess」においては、ループを使う場合、for-ofarray.forEach() を使用すれば、インデックス記法でアクセスした場合のundefined型とのユニオン型として解釈される制約を受けないということで、それらを積極的に使用することの検討をして下さい、なんて言って勧めてる。for-of にしろ forEach にしろ提供される value は Javascript によって undefined でないことが保証されているからだろうけれど、まぁ、Javascript の場合、オブジェクトにはキーがあっても未定義な要素というものが存在するわけで(下の方に記述)、for の場合はそれが undefined でない保証の責任はプログラマーが負って、とことん確認して保証できるならこの後出てくる ” ! ” を用意しといたからそれを使えってことなんだろうな。って、それはもう for を使うなっ!ってことなんだろう。なんだろな~、一番高速なんだけどな~!

というところで、もっと簡単にエラーを回避できる方法があるとのことで、それは ” ! ” である。
これは – non-null assertion operator – というのだそうで、コンパイラにこの値は null undefined ではないと伝えるものとのことだ。しかし、これってちょっと?な気がしないでもない。これってのはごまかしでしょ。手抜きの埋め合わせ的な。これを使うのなら、noUncheckedIndexedAccess オプション自体使わなくても良いような気がする。まぁ、でも、他に恩恵を受ける状況もあるのだろうし、変数の型の整合性をチェックしてくれる道具として上手く適当に付き合えば、という具合のものとしてはこれぐらいの方が、という気もする。たぶん、” ! ” は、どうにも困った場合以外は、あまり使わない方が良さげな感じがする。

ちなみに、意地悪く ” ! ” を指定しつつ、undefined となる存在しないはずの key を試しに指定してみると、TypeScript においてはエラーとなることはなく難無くコンパイルできる(当然か!)。実際に動かしてみると Javascript は当然エラーとなる(そりゃそうだ!)。

const sizespecify: string[][] = [ [ '_s', '_760' ], [ '_ss', '_400' ] ];

for ( let i = 0; i < 2; ++i ) {
    if ( winwidth > 399 ) {
        limgsrc[ i ] = themeurl + 'img/' + loadimg[ i ] + sizespecify[0]![ i ] + '.webp';// not error
    } else {
        limgsrc[ i ] = themeurl + 'img/' + loadimg[ i ] + sizespecify[1]![ i ] + '.webp';// not error
    }
}
TypeScript
CopyExpand

Array や Object のいろいろな場合

たぶん、ちょっとさわらないでいるとほとんど忘れてしまうのであろうから、すぐに見直せるように配列、連想配列の場合のエラーの出ない書き方などを残しておいたほうが良さそう。

const na: Array<Array<number>> = [ [ 0, 1 ], [ 2, 3 ] ];

const nb: number | undefined = na[1]![1];
    // ! がないと na[1] が error -> Object is possibly 'undefined'.

const nc: number = 'number' === typeof nb ? nb : 0;
    // nc: number = nb; とやると当然のことながら error となり、Type 'number | undefined' is not assignable to type 'number'. と怒られる
TypeScript
CopyExpand

で、この場合に ” ! ” を使わずにはどうすれば!と、色々試していると下の方法で大丈夫そう。

const na: Array<Array<number>> = [ [ 0, 1 ], [ 2, 3 ] ];
    
const nb: number[] | undefined= na[1];
    // nb: number[] = na[1]; とやると errorとなり、
    // Type 'number[] | undefined' is not assignable to type 'number[]'. と怒られる

let nc: number = 0;

if ( 'object' === typeof nb ) {// これがないとerror -> Object is possibly 'undefined'.
    nc = <number>nb[1];
    // nc = nb[1]; とやると error となり、
    // Type 'number[] | undefined' is not assignable to type 'number[]'. Type 'undefined' is not assignable to type 'number[]'.と怒られる
}
TypeScript
CopyExpand

配列の値を for roop の中で使い、別の配列の key として変数で使う

const keys: number[] = [ 2, 4, 6, 8, 9 ],
    ary: number[] = [];

for ( let i = 0, keys_len = keys.length; i < keys_len; ++i ) {
    const key: number | undefined = keys[ i ];

    // この if 文がないと key が error となる。
    // Type 'undefined' cannot be used as an index type. & Object is possibly 'undefined'.
    if ( 'number' === typeof key ) {
        ary[ key ] = i * key;
    }
}
TypeScript
CopyExpand

二次元の配列を for roop の中で使い、別の配列の key として変数で使う

const keys: Array<Array<number>> = [ [ 2, 4, 6, 8, 9 ], [ 1, 3, 5, 7, 9 ] ],
    ary: number[] = [];

for ( let i = 0, keys_len = keys[1]!.length; i < keys_len; ++i ) {// ! がないとerror
    const key: number | undefined = keys[1]![ i ];// ! がないとerror

    if ( 'number' === typeof key ) {// この条件文がないとerror
        ary[ key ] = i * key;
    }
}
TypeScript
CopyExpand

二次元の配列を for roop の中で使い、連想配列の key として変数で使う

const keys: number[][] = [ [ 2, 4, 6, 8, 9 ], [ 1, 3, 5, 7, 9 ] ],
    obj: { [key: string ]: string } = {};

for ( let i = 0, keys_len = keys[1]!.length; i < keys_len; ++i ) {// ! がないとerror
    const nmk: number | undefined = keys[1]![ i ],// ! がないとerror
        stk: string = String( nmk );

    if ( 'number' === typeof nmk ) {// この条件文がないとerror
        obj[ stk ] = String( i * nmk );
    }
}
TypeScript
CopyExpand

二次元の配列(文字列)を for roop の中で使い、連想配列の key として変数で使う

おまけ。
まぁ、interface を使っても同じこと。

interface objtype { [ key: string ]: string; };

const keys: Array<Array<string>> = [ [ 'a', 'b', 'c', 'd', 'e' ], [ 'v', 'w', 'x', 'y', 'z' ] ],
    obj: objtype = {};

for ( let i = 0, keys_len = keys[1]!.length; i < keys_len; ++i ) {// ! がないとerror
    const stk: string | undefined = keys[1]![ i ];// ! がないとerror ->  Object is possibly 'undefined'.

    if ( 'string' === typeof stk ) {// これがないとerror ->  Object is possibly 'undefined'.
        obj[ stk ] = ( i * 9 ) + stk;
    }
}
TypeScript
CopyExpand

ふぅ~、今のところとりあえず以上。
遅いとわかっている for-of foreach をわざわざ使う気にならず、最速の for を使うことにこだわってはいるのだけれど、そのために余計な条件式を加えなければならないのなら、for-of を使ってもそんなに違いはなくなってしまうのかもしれない。

なんだかんだと言いつつ、VScord のお世話になっているのであるけれど、今回のように既存の Javascript ファイルの書き直しのようなことをやっていると、まだ手を付けていない部分の Typescript のエラーは当然たくさん存在しているわけで、そのようにたくさんエラーが存在している状況だと、どうやらエラーの表示の挙動がおかしくなっている気もする。エラーがどうにもならないときは、面倒でもまっさらなファイルにその部分だけを抜き出して試したほうが間違いなく良い結果を得ることができると思う。当然と言えば当然のことではあるけれど。

noUncheckedIndexedAcces は必要なのか

はたしてこの noUncheckedIndexedAcces というオプションを使う必要はあるのだろうか?
配列や連想配列といったオブジェクトの要素が undefined となるのはいかなるときなのだろうか。たとえば、こんなことをすれば当然 undefined になるのだけれど。

const ary = [ 1, 2, 3 ];
console.log( ary[3] );// 存在する要素の key は 0,1,2 だけ
JavaScript
CopyExpand

しかし、こんなのは想定外のプログラムミスというより、ケアレスミスによるタイプミスとかで、そもそもこんな間違いをしているのならプログラム全体がまともに動くものとは思えない。
唯一、思いつく状況といえば、次のような場合なのだろうが。

const ary =[];

ary[1] = 1;
ary[3] = 2;
ary[6] = 3;

for ( let i = 0, ary_len = ary.length; i < ary_len; ++i ) {
    console.log( '[', i, ']', '->', ary[ i ] );
}

// [0] -> undefined
// [1] -> 1
// [2] -> undefined
// [3] -> 2
// [4] -> undefined
// [5] -> undefined
// [6] -> 3
JavaScript
CopyExpand

これはありえそうではあるけれど。特に不明な要素からなる配列を元にして for などで dynamic に新しい配列を作成した場合などに。まぁ、でもよくわかっている分にはこれで落とし穴にはまることもありはしない。
オブジェクトの場合もちょっと無理やりだけれど(普通こんなことはしそうもないけれど可能性として)、なくはないのかも。

const obj = {};

let ud;

obj[1] = 'a';
obj['ud'] = ud;
obj['df'] = 'b';

for ( const val in obj ) {
    console.log( val, '->', obj[ val ] );
}

// 1 -> a 
// ud -> undefined 
// df -> b
JavaScript
CopyExpand

まぁ、たぶんこれらの undefined の可能性なのじゃないだろうか。
それにしても、TypeScript には readonly なんて機能がせっかく用意されているのだから、これをもっとうまく使えるようにすれば、もっとスッキリ書ける状況も多くあったような気がするのだけれど。

固定された配列(連想配列)をもとにして、その配列の個数を Array.length でもとめて for などでループさせる場合に、” ! – non-null assertion operator – ” を使ってエラーを回避するのであれば、そもそも noUncheckedIndexedAcces を使う必要な全くないと思われる。
このオプションを使用しなければ、現実的に必要のない条件式なども省くことができるわけで、自的に一人で書いている分にはどうも必要なさげなのではあるまいか。

ただし、TypeScript を学んで接してみることは、それ以後使わない選択をするにしても、自然と変数の型などの意識を持ちつつコードを書くようになったので、そういう意味においては知ってみて良かったと思う。なにせ Rust なんかは型においてもっとガチガチであるわけだし。

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=170700

Sanbanse Funabashi
2010.10.24 sunrise

Top

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