音楽サーバ"Mopidy"のフロントエンドを作る:08 TypeScript用の型定義をつくる
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第8回です。
今回は、TypeScriptでライブラリを使う際の、型定義について、です。
大正義@types
前回記事でちらっと触れましたが、最近はライブラリ配布時にTypeScript用の型定義も、同梱していることが散見されるようになりました。
しかし、たとえ同梱の定義が無いとしても。
有名なライブラリを網羅する、DefinitelyTypedというリポジトリ があります。
そして、それをnpmパッケージとして取り込んだものが、@typesです。
もはや、自分で型定義を書くことなんて無いのです!
...そう。
無いのです。定義済みの型定義で用事が済むのであれば...。
プロパティが足りない!
Mopidy.Finderでは、プレイリスト編集機能のためにSortableJSを導入しました。
ドラッグ&ドロップで要素を並べ替えできる、とても便利なライブラリです。
ここで、その@types上の定義を見てみましょう。
npmパッケージ名は、@types/sortablejs
です。
インストールして、中身を除いてみます。
# npm install -D @types/sortablejs
設定値をセットするための、Options定義がこちら。
node_modules/@types/sortablejs/index.d.ts:
declare namespace Sortable { export interface Options { /** * ms, animation speed moving items when sorting, `0` — without animation */ animation?: number; /** * Class name for the chosen item */ chosenClass?: string; dataIdAttr?: string; /** * time in milliseconds to define when the sorting should start */ delay?: number; /** * Disables the sortable if set to true. */ disabled?: boolean; /** * Class name for the dragging item */ dragClass?: string; /** * Specifies which items inside the element should be draggable */ draggable?: string; dragoverBubble?: boolean; dropBubble?: boolean; /** * Class name for the cloned DOM Element when using forceFallback */ fallbackClass?: string; /** * Appends the cloned DOM Element into the Document's Body */ fallbackOnBody?: boolean; /** * Specify in pixels how far the mouse should move before it's considered as a drag. */ fallbackTolerance?: number; fallbackOffset?: { x: number, y: number }; /** * Selectors that do not lead to dragging (String or Function) */ filter?: string | ((this: Sortable, event: Event | TouchEvent, target: HTMLElement, sortable: Sortable) => boolean); /** * ignore the HTML5 DnD behaviour and force the fallback to kick in */ forceFallback?: boolean; /** * Class name for the drop placeholder */ ghostClass?: string; /** * To drag elements from one list into another, both lists must have the same group value. * You can also define whether lists can give away, give and keep a copy (clone), and receive elements. */ group?: string | GroupOptions; /** * Drag handle selector within list items */ handle?: string; ignore?: string; /** * Call `event.preventDefault()` when triggered `filter` */ preventOnFilter?: boolean; scroll?: boolean; /** * if you have custom scrollbar scrollFn may be used for autoscrolling */ scrollFn?: ((this: Sortable, offsetX: number, offsetY: number, event: MouseEvent) => void); /** * px, how near the mouse must be to an edge to start scrolling. */ scrollSensitivity?: number; /** * px */ scrollSpeed?: number; /** * sorting inside list */ sort?: boolean; store?: { get: (sortable: Sortable) => string[]; set: (sortable: Sortable) => void; }; setData?: (dataTransfer: DataTransfer, draggedElement: HTMLElement) => void; /** * Element dragging started */ onStart?: (event: SortableEvent) => void; /** * Element dragging ended */ onEnd?: (event: SortableEvent) => void; /** * Element is dropped into the list from another list */ onAdd?: (event: SortableEvent) => void; /** * Created a clone of an element */ onClone?: (event: SortableEvent) => void; /** * Element is chosen */ onChoose?: (event: SortableEvent) => void; /** * Element is unchosen */ onUnchoose?: (event: SortableEvent) => void; /** * Changed sorting within list */ onUpdate?: (event: SortableEvent) => void; /** * Called by any change to the list (add / update / remove) */ onSort?: (event: SortableEvent) => void; /** * Element is removed from the list into another list */ onRemove?: (event: SortableEvent) => void; /** * Attempt to drag a filtered element */ onFilter?: (event: SortableEvent) => void; /** * Event when you move an item in the list or between lists */ onMove?: (event: MoveEvent) => boolean; } }
プレイリストの編集時は、複数のトラックを選択して並べ替えをしたい。
なので、MultiDragプラグインを使いたい。
こちらの公式サンプルコードでは、このように書かれています。
new Sortable(multiDragDemo, { multiDrag: true, // Enable multi-drag selectedClass: 'selected', // The class applied to the selected items animation: 150 });
しかし、上述のオプション定義には、multiDrag
項目が見当たらないのです...。
このままTypeScriptでサンプルのように書いてしまうと、ビルドエラーになります。
こんな場合は、慌てず騒がず。
型定義ファイルに、手を入れればいいのです!
型定義ファイルを配置する
node_modules下のファイルに直接手を入れるのは、ご法度です。
他の環境ではビルド出来なくなってしまいますね。
ではどうするか、というと。
プロジェクトに型定義ファイル用のフォルダを作り、型定義ファイルをコピーします。
Mopidy.Finderのプロジェクトでは、型定義配置用のフォルダtypes
を作り、そこにライブラリ別に配置しています。
SortableJSの型定義は、types/sortable/index.d.ts
に配置しました。
そして、元になった@typesのパッケージをアンインストールします。
# npm uninstall -D @types/sortablejs
その上で、TypeScriptに、コピーした型定義ファイルを参照させるように設定します。
tsconfig.json
のcompilerOptions
の中で、baseUrl
とpaths
を書きます。
"compilerOptions": { // -- 中略 -- "baseUrl": "./", "paths": { "sortablejs/modular/sortable.complete.esm": [ "types/sortable" ] } }
paths
を使用する際は、そのパスの基準となるbaseUrl
が必要になるようです。
ここでは、baseUrl
はプロジェクトルートを示す./
(=tsconfig.jsonがあるフォルダ)としています。
実際にSortableJSを使うTypeScriptモジュールでは、下記のようにimportして使います。
import Sortable from 'sortablejs/modular/sortable.complete.esm'; const elem = document.querySelector('.sortableWrapper'); Sortable.create(elem , { // 各種オプション定義 });
importするモジュールを、単にsortablejs
でなくsortablejs/modular/sortable.complete.esm
としてあるのは、SortableJSの全プラグインが同梱されたsortable.complete.esm.js
を使うためです。
公式ページのこちらに記載がありますね。
※Visual Studioでは、型定義の参照先を変えた際、認識されない場合があります。※
※そのときは、Visual Studioを再起動してみてください。※
型定義ファイルに手を入れる
さて、型定義を好き放題にいじるための準備が整いました!
そこで、公式ドキュメントに書いてある、MultiDragプラグインの定義を読んでみます。
まず、Option。
- multiDrag
- selectedClass
- multiDragKey
- onSelect
- onDeselect
の5つの定義が増えたようです。
multiDrag
は、複数選択可否の設定です。
これをtrue
とするとで、複数選択可能な状態にできる、と。
selectedClass
は、選択したDom要素に付与するクラス名ですね。
選択状態にした際にCSSによって背景色を変えたいので、これは使えそうです。
multiDragKey
は、複数選択時にキーボードで押すキーのようです。
常時複数選択にするつもりなので、これは使いませんでした。
また、onSelect
とonDeselect
のイベントハンドル2つ。
これは選択時と選択解除時に発火するイベントのようですね。
それぞれの時点でDomを操作するときに使えるかも。
でも、クラス名を自動で付与してくれるなら、使わないかな?
というわけで、Optionの定義に、multiDrag
とselectedClass
を追加しました。
declare namespace Sortable { export interface Options { // -- 中略 -- multiDrag?: boolean; selectedClass?: string; } }
そして、イベントオブジェクトのほうにも、値が増えています。
- items
- clones
- oldIndicies
- newIndicies
items
はDom要素の配列が入るようですね。
clone
は...なんだろう?分からないまま、使わずじまいでした。
そして、oldIndicies
とnewIndicies
。これは、Dom要素ごとの順序インデックスを、新旧で持っているようです。
イベント定義記述のすぐ下に、Indiciesの定義が書いてありますね。
それらを、Eventの定義に追記していきます。
declare namespace Sortable { export interface SortableEvent extends Event { // -- 中略 -- items: HTMLElement[]; newIndicies: Index[] | undefined; oldIndicies: Index[] | undefined; } export interface Index { index: number; multiDragElement: HTMLElement } }
なお、ドキュメント上のIndex
の定義ではHTMLElementはelement
、と書いてます。
しかし実際にイベントをダンプしてみると、名前はmultiDragElement
のようです。
こういうのは、実装に合わせて修正していきます。
修正後の型定義でビルドしてみる
型定義の修正が終わったら、実際にコードを書いて確かめます。
SortableJSの生成箇所は、こんなコードになっています。
src/ts/Views/Playlists/Lists/Tracks/TrackList.ts:
private async SetSortable(): Promise<boolean> { this.DisposeSortable(); await Delay.Wait(10); this.sortable = Sortable.create(this.TrackListUl, { animation: 500, multiDrag: true, selectedClass: 'selected', dataIdAttr: 'data-uri', onEnd: (): void => { this.OnOrderChanged(); } }); return true; }
TypeScriptをビルドしてみると...
はい、きちんと通ります!
プレイリスト画面で、実際に並べ替えを試してみます。
複数を選択して...
ドラッグすると...
順番が、変わりました!
以上、TypeScript型定義のメンテナンスのおはなし、でした!