TypeScript HTML要素取得時の型引数指定と型アサーション(キャストのようなもの)

HTML要素をdocument.querySelectorAll()で取得してforEachかけ、要素の属性値を確認しようとしたところ

プロパティ 'dataset' は型 'Element' に存在しません。

と怒られた。Element?

HTMLElementにキャストとかすればいいのか、と思って検索すると、型アサーションの書き方がひっかかる。
アサーションってなんだろうと思ってたけどキャスト的なやつだったのか。

Type Assertion(型アサーション) - TypeScript Deep Dive 日本語版

手を加えたらエラーが消えたが…

document.querySelectorAll("[data-msgbox]").forEach(($el)=>{
  // const msg = $el.dataset.msgbox; // プロパティ 'dataset' は型 'Element' に存在しません。
  // const msg = (<HTMLElement>$el).dataset.msgbox; // 型アサーション
  const msg = ($el as HTMLElement).dataset.msgbox; // 同上
  console.log('msg',msg);
});

要素の取得メソッドに型引数を指定する

さらに調べてみるともっと良い書き方があった。

querySelector, querySelectorAll等のメソッドを実行する際、
あらかじめメソッドに型引数を指定するのが良いらしい。

TypeScript で querySelector メソッドを使うときに型引数を指定する - Hatena Developer Blog

これはいけません! これだと Element | null 型から HTMLElement 型にキャストすることになり、nullability が除去されてしまいます。このままコードを書き続けると、要素が見つからないときに実行時エラーを引き起こしかねません。

divの取得で確認してみたところ、確かに要素取得メソッド実行後の戻り値に型アサーションをかけると、結果がnullになることを考慮しない書き方となってしまう。(nullability という表現覚えました)
要素取得メソッドに型引数を渡した場合は型が自動的に HTMLElement | null となり、nullになる可能性を示してくれる。

const $div1 = document.querySelector("div") // const $div1: HTMLDivElement | null
const $div2 = document.querySelector("div") as HTMLElement //const $div2: HTMLElement
const $div3 = document.querySelector<HTMLElement>("div") // const $div3: HTMLElement | null

最初にあげた例の書き方としては以下がよりよい

document.querySelectorAll<HTMLElement>("[data-msgbox]").forEach(($el)=>{
  const msg = $el.dataset.msgbox;
  console.log('msg',msg);
});

正規表現の覚え書き

忘れそうなやつをメモ

先読み・後読み

たまにしか使わないので混乱する…
先読みは「指定パターンの先(前方)の位置」をあらわし
後読みは「指定パターンの後(後方)の位置」をあらわす

「肯定先読み」という記法の呼び名の語順に沿って一覧
(「先読み肯定」「先読み否定」のほうが頭の中での分類の仕方に合致するけど)

  • 肯定
    • 先読み (?=pattern)
    • 後読み (?<=pattern)
  • 否定
    • 先読み (?!pattern)
    • 後読み (?<!pattern)

後読みの<を矢印に見立てるとパターンの位置が直感的にわかる
"1234".replace(/(?<=12)/,"-") なら
「"12"の後 = "12"がすぐ左側<にくる位置」というイメージ

jsの例

"1234".replace(/(?=34)/,"-"); //'12-34'
"1234".replace(/(?<=12)/,"-") //'12-34'
"es2016 eslint escape".replace(/es(?!cape)/g,"") 
// '2016 lint escape'
"ecmascript javascript typescript".replace(/(?<!type)script/g,"") 
//'ecma java typescript' 

phpエスケープシーケンス \K

後読みチックな書き換えができる

<?php
$url = "https://hostname.domain.name";
$auth = "user:pass@";

$urlWithAuth = preg_replace("/https:\/\/\K/", $auth, $url); 
$urlWithAuth = preg_replace("/(?<=https:\/\/)/", $auth, $url); 
// どちらも結果は 'https://user:pass@hostname.domain.name'

PHP: エスケープシーケンス - Manual

\K を使用すると、マッチの開始位置をリセットできます。 たとえば、パターン foo\Kbar は "foobar" にマッチしますが、 結果は "bar" にマッチしたと報告されます。 \K を使用しても、キャプチャした部分文字列には影響を及ぼしません。 たとえば、パターン (foo)\Kbar が "foobar" にマッチしたときの最初の部分文字列は "foo" です。

PHP クラス定数取得

クラス定数ってstatic変数みたいに、インスタンス化しなくてもクラスから直でとれるんだ…。 どちらも::で呼び出すの一緒だなと思ってはいたけど。 定数はインスタンス化しないととれないものと思い込んでいた。

余談:静的メンバって呼び方基本とするなら、変数は静的変数、と呼ぶのが自然だけど、 static変数のがわかりやすいよね…静的メンバはstaticメンバっていったほうが直感的かもな

知らずに色々検証してしまった。

以下いらなくなっちゃったけど記録として…

<?php
class TestClass
{
  const A = 1;
  const B = 2;
  const C = 3;


  // * 定数の出力 
  // ひとつずつ
  public static function getConst($propName)
  {
    return constant("self::" . $propName);
  }
  // 配列取得
  private static $constants;
  public static function getConstants()
  {
    if (!static::$constants) {
      $reflectionClass = new ReflectionClass(__CLASS__);
      static::$constants = $reflectionClass->getConstants();
    }
    return static::$constants;
  }
}
var_dump(TestClass::getConstants());
var_dump(TestClass::getConst("A"));

// まあこれでいけたんだけど…
var_dump(TestClass::A);

おまけ

いまとなっては没だけど、最初はこういうのも考えていた。 この取得方法の場合、定数名が間違っていても返される値がNULLになるだけで何も起きないので、エラーを送出するように。

<?php
    function getConst($propName)
    {
      $const =  TestClass::getConstants();
      if (!$const[$propName]) throw new ErrorException('TestClassに"' . $propName . '"が存在しません。');
      return $const[$propName];
    }

PHP クラスの静的メンバ挙動を通して諸々覚える

ダブルコロンのスコープ定義演算子

$foo::BAR とか Foo::$bar とか
PHPのコロンふたつの記法、なんとなく得体がしれずスルーし続けてきたが、自作クラスに定数を定義した際はじめて、定数や静的メンバの呼び出しに用いるものと知る。
インスタンスメンバへのアクセスにつかう -> アロー演算子 では定数を呼び出せないことから気づいた。

PHP: スコープ定義演算子 (::) - Manual

static を用いたクラスの検証

そもそもオブジェクト指向におけるstatic修飾子の効果や静的メンバの概念が曖昧だった。
(jsなどでは定義済みの静的メンバを頻繁かつ無意識に使用していたけど) 自分で定義してみることでようやくしっかり把握できました。

デバッグついでに、あまり使ったことのなかったマジック定数の出力結果も確認。 さらにマジック定数の変化を知るために traitnamespace の使い方も確認。

わからないことが掘れば掘るほど芋づる式にでてきてキリがない。 あまりPHPで凝った処理書いたことがないからなあ…。

トレイト

使ったことないどころか今日初めて知った単語。
処理をまとめられる、sassのmixinのようなもの。 名前空間とかトレイトとか、がっっつりPHP組まなきゃならない場合にうまく使えたら便利そうだけど、 下手するとかえってややこしくなりそう。

<?php
trait classOutputTest
{
  static function getClassname()
  {
    return __CLASS__;
  }
  // static $msg = '';
  static $counter = 0;
  static $msg_in_trait = '👻 $msg_in_trait in <span style="color:#fff;background:blue;">' . __CLASS__ . '</span>';
  static function getMsgInTrait()
  {
    return '⛄ getMsgInTrait() in <span style="color:#fff;background:red;">' . __CLASS__ . '</span>';
  }
}

クラス

手抜きしてstatic宣言にトレイト classOutputTest を使いまわす。(これにより予期せぬ結果となったので後述)

<?php
class Bird
{
  function __construct()
  {
    static::outputTest();
  }
  static function outputTest($classname=__CLASS__)
  {
    echo "==================================" . PHP_EOL;
    if (property_exists(__CLASS__, 'msg')) {
      echo '<strong> ' . static::$msg . '</strong>' . PHP_EOL;
    }
    echo 'Bird & Birdを継承したインスタンス数: ' . ++self::$counter . PHP_EOL;
    echo '__CLASS__ ➡ ' . __CLASS__ . PHP_EOL;
    echo '$classname ➡ ' . $classname . PHP_EOL;
    echo '__TRAIT__ ➡ ' . __TRAIT__ . PHP_EOL;
    echo '__METHOD__ ➡ ' . __METHOD__ . PHP_EOL;
    echo '__FUNCTION__ ➡ ' . __FUNCTION__ . PHP_EOL;
    echo '__NAMESPACE__(クラス直下関数に記述) ➡ ' . static::getNamespace() . PHP_EOL;
    echo '__NAMESPACE__(trait内に記述) ➡ ' . __NAMESPACE__ . PHP_EOL;
    echo PHP_EOL.'⚙ 以下トレイト classOutputTest で全クラス一括定義した静的メンバ ⚙'.PHP_EOL;
    echo 'static::getMsgInTrait()  ➡ ' . static::getMsgInTrait() . PHP_EOL;
    echo 'static::$msg_in_trait ➡ ' . static::$msg_in_trait . PHP_EOL;
  }
  
  static function getNamespace()
  {
    return __NAMESPACE__;
  }
  use classOutputTest;
  static $msg = "🐣 大元となる [Birdクラス]";
}

class Chicken extends Bird
{
  use classOutputTest;
  static $msg = "🐓 Birdを継承 [Chickenクラス]";
}

namespaceの概念理解用に、名前空間を新規に作成してBirdを継承したPenguin,Kiwiクラスを作成

<?php
// # namespace :すべての処理にさきがけて宣言する必要がある
namespace TOBENAI_TORI;

trait tobenaiTrait{
  function __construct()
  {
    static::outputTest(__CLASS__);
    echo PHP_EOL.'⚙ 名前空間 ['.__NAMESPACE__.'] 内のトレイト tobenaiTrait によるコンストラクタ追加処理 ⚙'.PHP_EOL;
    echo '__TRAIT__ ➡ ' . __TRAIT__ . PHP_EOL;
    echo '__METHOD__ ➡ ' . __METHOD__ . PHP_EOL;
    echo '__FUNCTION__ ➡ ' . __FUNCTION__ . PHP_EOL;
    echo '__NAMESPACE__ ➡ ' . __NAMESPACE__ . PHP_EOL;

  }
  static function getNamespace(){//ここで親クラスの処理上書きすると名前空間が出力される(なくすと空に)
    return __NAMESPACE__;
  }
}

class Penguin extends \Bird // \でグローバル名前空間を指定
{
  use \classOutputTest, tobenaiTrait;

  static $msg ="🐧 名前空間 [TOBENAI_TORI] 、グローバル空間のBirdを継承した [Penguinクラス] ";
}
class Kiwi extends \Bird // \でグローバル名前空間を指定
{
  use \classOutputTest, tobenaiTrait;

  static $msg ="🥝 名前空間 [TOBENAI_TORI] 、グローバル空間のBirdを継承した [Kiwiクラス] ";
}

これらを全部読み込んで実行

<?php
    $bird = new Bird();
    $chicken = new Chicken();
    $ns_penguin = '\TOBENAI_TORI\Penguin';
    $tobenai_peguin = new $ns_penguin();
    $ns_kiwi = '\TOBENAI_TORI\Kiwi';
    $tobenai_kiwi  = new $ns_kiwi();

名前空間名のみを変数化してクラス名と足したかったけど、どう書けばいいのかわからなかった。

出力結果

==================================
 🐣 大元となる [Birdクラス]
Bird & Birdを継承したインスタンス数: 1
__CLASS__ ➡ Bird
$classname ➡ Bird
__TRAIT__ ➡ 
__METHOD__ ➡ Bird::outputTest
__FUNCTION__ ➡ outputTest
__NAMESPACE__(クラス直下関数に記述) ➡ 
__NAMESPACE__(trait内に記述) ➡ 

⚙ 以下トレイト classOutputTest で全クラス一括定義した静的メンバ ⚙
static::getMsgInTrait()  ➡ ⛄ getMsgInTrait() in Bird
static::$msg_in_trait ➡ 👻 $msg_in_trait in Bird
==================================
 🐓 Birdを継承 [Chickenクラス]
Bird & Birdを継承したインスタンス数: 2
__CLASS__ ➡ Bird
$classname ➡ Bird
__TRAIT__ ➡ 
__METHOD__ ➡ Bird::outputTest
__FUNCTION__ ➡ outputTest
__NAMESPACE__(クラス直下関数に記述) ➡ 
__NAMESPACE__(trait内に記述) ➡ 

⚙ 以下トレイト classOutputTest で全クラス一括定義した静的メンバ ⚙
static::getMsgInTrait()  ➡ ⛄ getMsgInTrait() in Chicken
static::$msg_in_trait ➡ 👻 $msg_in_trait in Bird
==================================
 🐧 名前空間 [TOBENAI_TORI] 、グローバル名前空間のBirdを継承した [Penguinクラス] 
Bird & Birdを継承したインスタンス数: 3
__CLASS__ ➡ Bird
$classname ➡ TOBENAI_TORI\Penguin
__TRAIT__ ➡ 
__METHOD__ ➡ Bird::outputTest
__FUNCTION__ ➡ outputTest
__NAMESPACE__(クラス直下関数に記述) ➡ TOBENAI_TORI
__NAMESPACE__(trait内に記述) ➡ 

⚙ 以下トレイト classOutputTest で全クラス一括定義した静的メンバ ⚙
static::getMsgInTrait()  ➡ ⛄ getMsgInTrait() in TOBENAI_TORI\Penguin
static::$msg_in_trait ➡ 👻 $msg_in_trait in Bird

⚙ 名前空間 [TOBENAI_TORI] 内のトレイト tobenaiTrait によるコンストラクタ追加処理 ⚙
__TRAIT__ ➡ TOBENAI_TORI\tobenaiTrait
__METHOD__ ➡ TOBENAI_TORI\tobenaiTrait::__construct
__FUNCTION__ ➡ __construct
__NAMESPACE__ ➡ TOBENAI_TORI
==================================
 🥝 名前空間 [TOBENAI_TORI] 、グローバル名前空間のBirdを継承した [Kiwiクラス] 
Bird & Birdを継承したインスタンス数: 4
__CLASS__ ➡ Bird
$classname ➡ TOBENAI_TORI\Kiwi
__TRAIT__ ➡ 
__METHOD__ ➡ Bird::outputTest
__FUNCTION__ ➡ outputTest
__NAMESPACE__(クラス直下関数に記述) ➡ TOBENAI_TORI
__NAMESPACE__(trait内に記述) ➡ 

⚙ 以下トレイト classOutputTest で全クラス一括定義した静的メンバ ⚙
static::getMsgInTrait()  ➡ ⛄ getMsgInTrait() in TOBENAI_TORI\Kiwi
static::$msg_in_trait ➡ 👻 $msg_in_trait in Bird

⚙ 名前空間 [TOBENAI_TORI] 内のトレイト tobenaiTrait によるコンストラクタ追加処理 ⚙
__TRAIT__ ➡ TOBENAI_TORI\tobenaiTrait
__METHOD__ ➡ TOBENAI_TORI\tobenaiTrait::__construct
__FUNCTION__ ➡ __construct
__NAMESPACE__ ➡ TOBENAI_TORI

まとめ

  • グローバル空間は\で指定可能
  • __NAMESPACE__namespace hoge; と同一ファイル上で出力するとその名前空間名が出力される。
    たとえば任意の名前空間配下で関数を宣言し、中身だけ別ファイルにしたとする。別ファイル上で__NAMESPACE__を出力すると空っぽになる。
  • 子クラスで静的メンバを上書きして参照したい場合は self::$hoge でなく static::$hoge。selfだと最初に定義したクラスのメンバが参照される。
  • クラスメンバの存在チェック property_exists(__CLASS__, 'msg')

予期せぬ現象 (子クラスの静的メンバをクラス直下にきちんと書けば問題ないようなんだけども)

親クラス・子クラスともにトレイトで静的プロパティ一括定義したところ、気になる現象がおきた。

  • トレイト内で宣言した静的プロパティ($msg_in_trait)が、 static::$msg_in_trait で呼び出しているにもかかわらずすべて親クラスの内容に統一される
  • 同じくトレイト内で宣言している関数については、それぞれのクラスごとの内容で実行される

参考

むず…

Promiseの基本的な使い方について

setTimeout 使った処理調整しようとして検索かけてる最中、下記の記事にぶちあたって脱線しました。

setTimeout はダサいぞ。JavaScript Promise を使って処理を順番に実行しよう

そういえば Promise ちゃんと理解していなかったっけ…。

基本的な記述

let executor = (resolve, reject) => {
  console.log("👻1");
  //resolve,  rejectいずれかを実行しないと後述のthenの処理がはしらず
  resolve("A");
};
let promiseObj = new Promise(executor);
promiseObj
  .then(
    // executor内のresolve,(reject)に渡した引数の内容が
    // promiseオブジェクトのthen()で実行する関数に渡る。
    // この例ではstrとしています。 イベントオブジェクトみたい。
    (str) => {
      console.log("👻2: 受け取った値 …", str); // A
      // returnするのがPromiseオブジェクトのとき、
      // コンストラクタ内の処理が完了するまで処理がとまる
      // この場合は一秒後のresolve()実行で次のthen()にうつる
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(str + "B");
        }, 1000);
      });
    }
  )
  .then((str) => {
    console.log("👻3: 受け取った値 …", str); // AB
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        //ランダムで失敗させてみる
        let random = ~~(Math.random() * 3);
        if (random > 0) {
          resolve(str + "C");
        } else {
          reject(); //catchの処理へとぶ
        }
      }, 1000);
    });
  })
  .then((str) => {
    console.log("👻4: 受け取った値 …", str); // ABC
    return str + "D";
  })
  .then((str) => {
    console.log("👻5: 最終結果 …", str); // ABCD
  })
  .catch((str) => {
    console.log("👻失敗しちゃった");
  });

Promise を使う - JavaScript | MDN

たくさんあって全部は理解しきれていないけど

  • then 関数の引数はオプション(必須ではない)です。また、catch(failureCallback) は then(null, failureCallback) の短縮形です。

  • 重要: コールバック関数から処理結果を返すのを忘れないでください。さもないと後続のコールバック関数からその処理結果を利用することができなくなります (アロー関数を使った () => x は () => { return x; } の短縮形です)。

思い出すのは$.ajax()の戻り値

Promise がもってる then()ってメソッド jQueryajax 処理でよく使うし機能も一緒。
でも$.ajax() の戻り値 は Promise オブジェクト とは違うらしい?

toString.call(new Promise(()=>{})) // Promise
toString.call($.ajax()) // Object

Object だった。 Promise 風に動くよう拡張された Object ってことだね。 以下公式の説明。

jQuery.ajax() | jQuery API Documentation

jQuery.ajax( url [, settings ] ) Returns: jqXHR

戻り値はjqXHRオブジェクト

The jqXHR objects returned by \$.ajax() as of jQuery 1.5 implement the Promise interface, giving them all the properties, methods, and behavior of a Promise

jqXHRPromiseのすべての機能をそなえる

参考・メモ

5年ぶりの投稿テスト

長らく放置していました。
web制作系のメモをこちらにまとめたくなったので動作のテスト。
はてなダイアリー時代に使用していたんですが、サービス終了ではてなブログに自動移行されていた。

当時はmarkdownでかきて~~と思いながらはてな記法覚えようとした記憶がありますが、今はmarkdownモードも選べるようだから嬉しいね。

今見ると未熟な内容の記事が多いんです。
これは常に繰り返すことなんでしょうね。
今書く内容は五年後にみたらなんじゃこりゃってなるに違いない。

せっかく記事にしたのに忘れちゃってる内容もいくつか…。

未熟でも記録はとっておくほうがいいね。
放置しすぎて記憶の溜まり場の中に埋もれきってしまったものを掘り返すことが楽しいです。

レスポンシブ(スマホ用)のコーディングメモ

  • margin,paddingの%指定は包含ブロック「幅」に対する割合
  • height,top,bottomや背景画像位置の%指定は包含ブロック「高さ」に対する割合
  • @media only screen and (max-width: 639px) {}
  • <meta name="viewport" content="width=device-width">