Try .NET Core

.NET Coreを動かした、試した記録を書き残します。

音楽サーバ"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.jsoncompilerOptionsの中で、baseUrlpathsを書きます。

"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は、複数選択時にキーボードで押すキーのようです。
常時複数選択にするつもりなので、これは使いませんでした。

また、onSelectonDeselectのイベントハンドル2つ。
これは選択時と選択解除時に発火するイベントのようですね。
それぞれの時点でDomを操作するときに使えるかも。
でも、クラス名を自動で付与してくれるなら、使わないかな?

というわけで、Optionの定義に、multiDragselectedClassを追加しました。

declare namespace Sortable {
    export interface Options {
        // -- 中略 --
        multiDrag?: boolean;
        selectedClass?: string;
    }
}

そして、イベントオブジェクトのほうにも、値が増えています。

  • items
  • clones
  • oldIndicies
  • newIndicies

itemsはDom要素の配列が入るようですね。
cloneは...なんだろう?分からないまま、使わずじまいでした。

そして、oldIndiciesnewIndicies。これは、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のようです。
f:id:try_dot_net_core:20190809161655p:plain
こういうのは、実装に合わせて修正していきます。

修正後の型定義でビルドしてみる

型定義の修正が終わったら、実際にコードを書いて確かめます。
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をビルドしてみると... f:id:try_dot_net_core:20190809162836p:plain
はい、きちんと通ります!

プレイリスト画面で、実際に並べ替えを試してみます。
複数を選択して... f:id:try_dot_net_core:20190809163451p:plain

ドラッグすると... f:id:try_dot_net_core:20190809163519p:plain

順番が、変わりました! f:id:try_dot_net_core:20190809163544p:plain

以上、TypeScript型定義のメンテナンスのおはなし、でした!