文字列を整形するクラスが2つあるとします。片方は先頭に > を付け、もう片方は英字を大文字に変換します。どちらも1件ずつ処理する format() を持っていますが、あとから文字列の配列をまとめて処理する機能も必要になりました。
ここで決めることは2つあります。呼び出し側がどんな型を前提にできるか。そして、一括処理のコードをどこで共有するかです。
interfaceで1件分の呼び出し方をそろえる
TextFormatter に、文字列を受け取って文字列を返す format() を定義します。
interface TextFormatter {
format(text: string): string;
}
class PrefixFormatter implements TextFormatter {
constructor(private readonly prefix: string) {}
format(text: string): string {
return `${this.prefix}${text}`;
}
}
class UppercaseFormatter implements TextFormatter {
format(text: string): string {
return text.toUpperCase();
}
}
implements TextFormatter を付けると、TypeScriptはクラスがこの型の条件を満たすか確認します。どちらのクラスのインスタンスも TextFormatter として扱えるので、呼び出し側は同じ format() を使えます。
Mixinで一括処理の実装を共有する
withBatchFormatting() は、インスタンスが TextFormatter の条件を満たすクラスを受け取り、formatAll() を持つ派生クラスを返すMixinです。上のコードに続けて書けます。
type FormatterConstructor = new (...args: any[]) => TextFormatter;
function withBatchFormatting<TBase extends FormatterConstructor>(Base: TBase) {
return class extends Base {
formatAll(texts: readonly string[]): string[] {
return texts.map((text) => this.format(text));
}
};
}
const BatchPrefixFormatter = withBatchFormatting(PrefixFormatter);
const BatchUppercaseFormatter = withBatchFormatting(UppercaseFormatter);
const texts = ["hello", "world"];
console.log(new BatchPrefixFormatter("> ").formatAll(texts));
// [ '> hello', '> world' ]
console.log(new BatchUppercaseFormatter().formatAll(texts));
// [ 'HELLO', 'WORLD' ]
formatAll() の中で this.format() を呼ぶので、配列を処理するコードは1か所にまとまり、各要素の整形には元のクラスの処理が使われます。
TextFormatter はMixinを適用できる条件にもなっています。format() を持たないクラスを withBatchFormatting() に渡すと型エラーになります。
コンストラクターの型も引き継がれます。制約にある any[] は、引数の異なるコンストラクターを受け付けるためのものです。ジェネリック型の TBase が元のクラス型を保持するため、BatchPrefixFormatter を生成するときは引数に文字列が必要です。
prototypeに後から追加する場合
JavaScriptでは、Object.assign() などで既存クラスの prototype にメソッドを追加できます。追加したメソッドをTypeScriptから元のクラスのメンバーとして扱う場合は、実行時の変更に合わせて型の情報も与える必要があります。
今回のようにMixinが派生クラスを返す形なら、TypeScriptは追加メソッドまで含めて型を推論します。たとえば、あとから前後の空白を取り除く TrimFormatter を作ったときも、withBatchFormatting(TrimFormatter) で同じ一括処理を組み込めます。