LoginSignup
6
2

More than 3 years have passed since last update.

分割代入で import もどき (JavaScript)

Last updated at Posted at 2019-11-20

はじめに

import と同じノリで、分割代入を使ってこんなのを書くことがしばしばある。

import風
const { floor, ceil } = Math;

とても便利。

なのだけれど、これは floorceil が単なる関数だからできる話。
オブジェクト/クラスのメソッドからも似たようなことできないかな、
と言うのが本題。

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); // オーバーロードが消える

個別に型を書くことはできなくもないけれど、うまい方法はないものか。

6
2
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2