Buri Memo:

アイデアや気づきとかが雑に書き殴られる

JavaScriptのProxyを使って、サードパーティーのクラスを書き換えずにログを出力したい

以下のようなサードパーティのクラスがあって、メソッドに渡された引数や結果をログに残すためにインターセプトしたい。

自分らが管理しているクラスなら、デコレーターを作るのがいいと思うが、サードパーティーのクラスを書き換えるのはちょっと嫌だ(保守性の低下や動作が壊れるリスク的な観点で)。

またクラス構造がネスト・継承されている場合、デコレーターが複雑になり保守もしにくくなりそうだなーと思っていた。

// thirdPartyClass.ts
interface Data {
    [Key: string]: number;
}

export class ThirdPartyClass {
    private data: Data = {};
    getData(key: string): number {
        return this.data[key];
    }

    setData(key: string, value: number): void {
        this.data[key] = value;
    }

    reset(): void {
        this.data = {};
    }
}

Proxy を使う

JavaScript には Proxy という、オブジェクトのプロキシを作成するコンストラクタが標準である。関数呼び出しであれば、 apply ハンドラーを使うことでインターセプトできる。

Proxy - JavaScript | MDN

import { ThirdPartyClass } from "./thirdPartyClass";

function wrap<M extends string, I extends Record<M, Function>>(instance: I, methods: M[]) {
    const result = {} as Pick<I, M>;
    for (const key of methods) {
        result[key] = new Proxy<I[M]>(instance[key], {
            apply: (target: I[M], _: any, argArray: any[]) => {
                const res = target.apply(instance, argArray);
                console.log(`args: ${argArray}, res: ${res}`);
                return res;
            },
        });
    }
    return result;
}

const obj = new ThirdPartyClass();
const wrappedObj = wrap(obj, ["getData", "setData"]);

wrappedObj.setData("my-key", 19); // args: my-key,19, res: undefined
wrappedObj.getData("my-key"); // args: my-key, res: 19

Generics について

また、Generics が少し複雑なのでコメントを入れてみる。
この generics によって、定義されていないメソッド名を指定したり、関数以外のプロパティが指定されると型エラーになる。

こんな複雑な generics も書けるのか~と驚いた。

function wrap<
    M extends string, // 制約: M は string
    I extends Record<M, Function>, // 制約: I[M] は Function
>(
    instance: I,
    methods: M[], // method=["add", "print"] の時 type M = "add" | "print"
) {
    /* 
        Pick では渡された I の0個以上のプロパティ M の型を作る

        # 例
        type I = {
            add: (a: number, b: number) => number;
            sub: (a: number, b: number) => number;
            print: (a: string) => string;
        };
        type M = "add" | "print";

        Pick<I, M> の型は以下になる
        {
            add: (a: number, b: number) => number;
            print: (a: string) => string;
        };
    */
    const result = {} as Pick<I, M>;
}

参考

今回のような複雑な generics がいくつか説明されていました。

www.digitalocean.com