TypeScript プロジェクトで Sortable.js を使用する際、「Property 'xxx' does not exist on type 'Sortable'」といった型エラーに悩まされたことはないだろうか?あるいは、設定オプションを記述するたびにドキュメントを確認しなければならず、開発効率が落ちているのではないだろうか。この記事では、Sortable.js を TypeScript と統合し、型安全性を確保するための実践的なアプローチを紹介する。
なぜ型定義が必要なのか?
Sortable.js は軽量かつ柔軟なドラッグ&ドロップライブラリだが、純粋な JavaScript で書かれており、公式の TypeScript 型定義(.d.ts)は提供されていない。そのため、TypeScript 環境では以下の問題が発生する:
- IDE の補完が効かない:API やオプション名を手動で記憶・確認する必要がある
- コンパイル時にエラーを検出できない:誤った設定やメソッド呼び出しが実行時まで気づかれない
- チーム開発での整合性が保ちにくい:型による契約がないため、誤解が生じやすい
例えば、以下のようなコードは TypeScript でエラーになる:
// コンパイルエラー
const instance = new Sortable(container);
instance.option('disabled', true); // ❌ option は存在しないと判定される
カスタム型定義ファイルの作成
解決策は、プロジェクト内に独自の型定義ファイルを作成することだ。src/types/sortable.d.ts(または任意のディレクトリ)に以下の内容を記述する:
declare module 'sortablejs' {
export default class Sortable {
constructor(element: HTMLElement, options?: Sortable.Options);
sort(order: string[]): void;
toArray(): string[];
destroy(): void;
option(name: string): any;
option(name: string, value: any): void;
static active: Sortable | null;
static dragged: HTMLElement | null;
static ghost: HTMLElement | null;
}
export namespace Sortable {
interface Options {
group?: string | GroupConfig;
sort?: boolean;
disabled?: boolean;
animation?: number;
ghostClass?: string;
chosenClass?: string;
dragClass?: string;
handle?: string;
draggable?: string;
onStart?: (evt: SortEvent) => void;
onEnd?: (evt: SortEvent) => void;
onAdd?: (evt: SortEvent) => void;
onUpdate?: (evt: SortEvent) => void;
onRemove?: (evt: SortEvent) => void;
onFilter?: (evt: SortEvent) => void;
}
interface GroupConfig {
name: string;
pull?: boolean | 'clone' | ((to: Sortable, from: Sortable) => boolean | 'clone');
put?: boolean | ((to: Sortable, from: Sortable) => boolean);
}
interface SortEvent {
to: HTMLElement;
from: HTMLElement;
item: HTMLElement;
clone?: HTMLElement;
oldIndex: number | null;
newIndex: number | null;
}
}
}
この定義により、コンストラクタ、インスタンスメソッド、設定オプション、イベントハンドラなどが型安全に扱えるようになる。
TypeScript での安全な使用例
型定義を配置した後、tsconfig.json の typeRoots または include 設定で認識させる。その後、以下のようにモジュールとしてインポートして使用できる:
import Sortable from 'sortablejs';
const containerA = document.getElementById('list-a')!;
const containerB = document.getElementById('list-b')!;
const sharedGroup = {
name: 'exchange',
pull: true,
put: (to: Sortable, from: Sortable) => to !== from,
};
new Sortable(containerA, {
group: sharedGroup,
animation: 120,
ghostClass: 'drag-ghost',
onEnd: (evt) => {
console.log(`移動元: ${evt.oldIndex}, 移動先: ${evt.newIndex}`);
},
});
new Sortable(containerB, {
group: sharedGroup,
animation: 120,
});
ここで重要なのは、すべてのプロパティやコールバックが型チェックされ、誤った引数やプロパティアクセスがコンパイル時に検出されることだ。
高度な型拡張テクニック
イベント型の拡張:独自のデータをイベントに付与したい場合、インターフェースを拡張できる:
interface ExtendedSortEvent extends Sortable.SortEvent {
metadata?: Record;
}
// 使用時は型アサーションまたはラッパー関数で対応
プラグイン対応:MultiDrag や Swap などの公式プラグインを使用する場合、モジュール拡張でオプションを追加できる:
declare module 'sortablejs' {
export namespace Sortable {
interface Options {
multiDrag?: boolean;
selectedClass?: string;
swapThreshold?: number;
invertSwap?: boolean;
}
}
}
フレームワークとの統合
React や Vue で使用する場合も、同様の型定義を活用できる。以下は React のカスタムフックの例:
import { useEffect, useRef } from 'react';
import Sortable from 'sortablejs';
interface UseSortableProps {
items: string[];
onReorder: (ids: string[]) => void;
}
export const useSortableList = ({ items, onReorder }: UseSortableProps) => {
const listRef = useRef<HTMLUListElement>(null);
useEffect(() => {
if (!listRef.current) return;
const sortable = new Sortable(listRef.current, {
animation: 100,
onEnd: () => {
const order = sortable.toArray();
onReorder(order); // string[] 型が保証される
},
});
return () => sortable.destroy();
}, [onReorder]);
return { listRef, items };
};
トラブルシューティング
- 型定義が古いバージョンと不一致:Sortable.js のソースコードを参照し、型定義を随時更新する
- 他の型定義と競合:
@types/sortablejsがインストールされていないか確認。存在する場合は削除し、カスタム定義を使用 - モジュール解決エラー:
tsconfig.jsonのcompilerOptions.moduleResolutionを"node"に設定
このように、手動で型定義を整備することで、Sortable.js を完全に型安全な状態で TypeScript プロジェクトに統合できる。将来的には公式で型定義が提供される可能性もあるが、現時点ではこの方法が最も確実かつ柔軟な対応策である。