はじめに
import
と同じノリで、分割代入を使ってこんなのを書くことがしばしばある。
import風
const { floor, ceil } = Math;
とても便利。
なのだけれど、これは floor
や ceil
が単なる関数だからできる話。
オブジェクト/クラスのメソッドからも似たようなことできないかな、
と言うのが本題。
from Object
同じことをしてもうまくいかない。
ダメな例
// 元々こんなクラスがあったとして
class Calculator {
constructor(initial) {
this.value = initial;
}
add(x) { this.value += x; }
sub(x) { this.value -= x; }
get() { return this.value; }
}
// 各メソッドを関数として扱ってしまいたいが...
const { add, get } = new Calculator(1);
add(2); // this が違う!
console.log(get()); // this が違う!
一々再定義したり bind
するのもめんどくさい。
めんどう
const c = new Calculator(1);
const add = c.add.bind(c); // bind したり...
const get = () => c.get(); // 再定義したり...
add(2);
console.log(get()); // 3
なので、こんなことをしてみる。
しかけ
const fromObject = x => new Proxy(x, {
get(x, k, r) {
const v = Reflect.get(x, k, r);
return typeof v === 'function' ? v.bind(x) : v;
}
});
Usage
const { add, get } = fromObject(new Calculator(1));
add(2);
console.log(get()); // 3
よし。
from Class (というかプロトタイプ)
メソッド (...args) => ret
を、
関数 (this, ...args) => ret
に変換して取り出す。
const fromClass = c => new Proxy(c.prototype, {
get(x, k, r) {
const v = Reflect.get(x, k, r);
return typeof v === 'function' ?
(self, ...args) => v.apply(self, args) :
self => Reflect.get(self, k);
}
});
関数でないものは getter に変換している。
Usage
const { toFixed } = fromClass(Number);
const { toUpperCase } = fromClass(String);
const { map, length: len } = fromClass(Array);
console.log(toFixed(10, 3)); // "10.000"
console.log(toUpperCase('hoge')); // "HOGE"
console.log(map([0, 1], x => x + 1)); // [1, 2]
console.log(len([0, 1, 2])); // 3
プロトタイプから抽出している都合上、
インスタンスにしか実体がないメソッドは取れないので注意。
from Class 別バージョン
メソッド (...args) => ret
を、
関数 (...args) => this => ret
に変換して取り出す。
this
をカリー化して後回し。
const fromCurried= c => new Proxy(c.prototype, {
get(x, k, r) {
const v = Reflect.get(x, k, r);
return typeof v === 'function' ?
(...args) => self => v.apply(self, args) :
self => Reflect.get(self, k);
}
});
こちらの方が配列操作やパイプライン等と相性が良いかもしれない。
Usage1
// これが
console.log([0,1,2].map( x => x.toFixed(3) ));
// こうも書ける
const { toFixed } = fromCurried(Number);
console.log([0,1,2].map( toFixed(3) ));
もちろん自作クラスなどでも。
Usage2
class MyClass {
constructor(name) {
this.name = name;
}
say(it) {
console.log(this.name + ": " + it);
}
}
const { say, name: getName } = fromCurried(MyClass);
const items = [
new MyClass("foo"),
new MyClass("bar"),
new MyClass("hoge"),
];
console.log(items.map(getName));
// > ["foo", "bar", "hoge"]
items.forEach(say('hello'));
// > "foo: hello"
// > "bar: hello"
// > "hoge: hello"
ついでに TypeScript 版
一応 TypeScript に持ちこめるけれど……
TypeScript
const fromObject = <T extends object>(x: T) => new Proxy(x, {
get(x, k, r) {
const v = Reflect.get(x, k, r);
return typeof v === 'function' ? v.bind(x) : v;
}
});
type FromClass<C extends { prototype: any }> = {
[P in keyof C["prototype"]]:
C["prototype"][P] extends
(...args: infer Args) => infer Ret ?
(self: C["prototype"], ...args: Args) => Ret :
(self: C["prototype"]) => C["prototype"][P];
};
const fromClass = <T extends { prototype: any }>(x: T): FromClass<T> =>
new Proxy(x.prototype, {
get(x, k, r) {
const v = Reflect.get(x, k, r);
return typeof v === 'function' ?
(self: any, ...args: any[]) => v.apply(self, args) :
(self: any) => Reflect.get(self, k);
}
});
type FromCurried<C extends { prototype: any }> = {
[P in keyof C["prototype"]]:
C["prototype"][P] extends
(...args: infer Args) => infer Ret ?
(...args: Args) => (self: C["prototype"]) => Ret :
(self: C["prototype"]) => C["prototype"][P];
};
const fromCurried = <T extends { prototype: any }>(x: T): FromCurried<T> =>
new Proxy(x.prototype, {
get(x, k, r) {
const v = Reflect.get(x, k, r);
return typeof v === 'function' ?
(...args: any[]) => (self: any) => v.apply(self, args) :
(self: any) => Reflect.get(self, k);
}
});
型変数とオーバーロードが消えてしまうので、使い勝手はいまいち。
const { map } = fromClass(Array); // any[] に対する操作になる
const { replace } = fromClass(String); // オーバーロードが消える
個別に型を書くことはできなくもないけれど、うまい方法はないものか。