#拡張メソッド とは
既存のライブラリ(たとえばNPMで取得した各種ライブラリ)、stringやnumberといった型に対して、変更を加えることなく、インスタンスメソッドや静的メソッドを追加するテクニックです。
簡単な例からいきましょう
#string にインスタンス拡張メソッドを作成
TypeScriptを利用して文字を取り扱う場合
let hoge = " AA;BB;CC";
console.log(hoge.trim());
const huga = "AA;BB;CC ";
console.log(huga.trim());
と string 型の 変数 や 引数 を利用しますね。
ちなみに、ES標準型は String で 内部的に String.prototype.length のような形で prototype上に定義され、私たちがTypeScriptで開発する場合は、d.ts 経由で(lib.es5.d.tsなど)それがあたかもインスタンスメソッドであるかのように扱えます。
同じことで、次の 3つの手順 を踏むことで既存の型に最初から存在したかのようなメソッドを作成できます
- 拡張したいオブジェクトと同名のインターフェイスを定義
- 定義に一致するメソッド本体の実装(prototype汚染を防ぎつつ)
- Bundleするようにimport する or 最初から headerに scriptタグ書いて読み込んでおく
#簡単な例のゴール
半角カタカナを判断する拡張メソッドのサンプル
simple string extensions
importもない状態で、インテリセンスまで効いた上で動いていますね。あたかも String インスタンスにもとからそういうメソッドがあったかのように。
ちなみに、拡張メソッドは pascal表記 が良いと思います。
区別しやすいのと、バッティング防止ですね。ライブラリ作成者とかぶったら困りますからね。
説明します
###拡張したいオブジェクトと同名のインターフェイスを定義
今回は String のインスタンスを拡張したいので、もとの定義を参照すると
とありますので、どこか適当なフォルダに同名のインターフェイスと実装したいメソッドを定義します。
これだけで、もう String にはメソッドが生えます。TypeScriptだから当然ですね。
###定義に一致するメソッド本体の実装(prototype汚染を防ぎつつ)
JavaScriptのprototypeに直接メソッドをはやすと思わぬ副作用が出ます。これを prototype 汚染といいます。
これを抑止した上で prototype を拡張する場合は Object.defineProperty を利用します。
詳細は上述リンクをご参照ください。
今回の実装は value:function として実装します。注意点はアロー関数だと this のスコープが変わるので、function で実装してください。
この中の this は String インスタンス本体を指し示します。呼び出し元だと困りますからね。
###Bundleするようにimport する or 最初から headerに scriptタグ書いて読み込んでおく
ここで作成した拡張メソッドは、各 tsx や ts で 個別に import はしません。
最初の例でも
import には style.css しか存在しませんが、正しくインテリセンスも仕事して、実行時もそれが呼び出されています。
TypeScript上でインテリセンスが仕事するのは、TSにインターフェイスがあるからで、あとはこの本体が ブラウザでの実行時に bundle されるか何かで読み込まれていればよいのです。
普通にHTMLなので HTML,CSS,JSで構成され、HTML上にJSがロードされる必要があります。ここら辺は JQuery 開発者であれば、何を当たり前のことを・・・と思うかもしれませんね。時代の流れですかね。
react であれば root 定義で読んどけば bundle されるので以降は気にせず好きな場所でimport気にせずに利用できます。
#ジェネリッククラスの拡張
例えば、配列に拡張メソッドを生やそうとすると Array<T>
が対象になります。
TS上は ジェネリック ですが、JSになれば何も関係ないので、同じ手順で同じように作成すると、配列に拡張メソッドが作成できます。
Chunk や GroupBy なんてあると便利なので、サンプル作ってみましょう。
simple array extensions
適当な場所に ジェネリック付きinterface 定義して、Object.defineProperty 上ではもはやJSの世界なので、型なく扱えます
#React で 実践的な拡張メソッド
たとえば トースト通知のライブライで有名なものに [notistack] (https://www.npmjs.com/package/notistack)があります
使い方もリンクの先にありますが
import { useSnackbar } from 'notistack';
const MyButton = () => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const handleClick = () => {
enqueueSnackbar('I love hooks');
};
return (
<Button onClick={handleClick}>Show snackbar</Button>
);
}
トースト通知を enqueueして 閉じるアクションで dequeue するみたいな、内部実装の気持ちはわかるのですが、使用する側からみて直観的ではないのと、冗長だと感じました。
notistack extensions
トーストをインスタンス化して、info や error などを呼び出せば適切な位置に適切なアイコンとともにメッセージを出すサンプルです。
銀の弾丸ではないので、何にでも使えるわけではないのですが、拡張メソッドを使うことでコードの凝縮性が高まる、プロジェクトで一貫した処理が行えるなどカバナンス効果などなど期待できることもあります
プロジェクトメンバーが好き勝手に生やすと困るので、ルールを決めてみんなで相談して生やしていければ、疎結合な開発資産もたまっていって、よいのではないでしょうか。