特定のプロパティを除外した新しいオブジェクトを作る(immutableなdelete)

あるオブジェクトから、特定のプロパティのみ除外した新しいオブジェクトを作りたい場合がある。

const p1 = {
  x: 0,
  y: 1,
  z: 2,
};

// p1からzプロパティを除外した新しいオブジェクトを作る

const p2 = Object.assign({}, p1);
delete p2.z;

console.log(p2); // { x: 0, y: 1 }


最近の実装(およびtranspile前提の環境)であればECMAScriptObject Rest Destructuringを使うことで、外部ライブラリやユーティリティ関数を用意しなくても1行で書くことができる。

const { z, ...p2 } = p1;

console.log(p2); // { x: 0, y: 1 }


格納されるプロパティのキーや取り除きたいプロパティのキーが動的な場合は、ComputedPropertyNameと組み合わせることになる。

const key = 'z';

const { [key]: _, ...p2 } = p1;

console.log(_); // 2
                // うーん……


取り除かれたプロパティの値は使わないよという表明で`_`にしたけど、そもそも余計な定数が定義されないようにしたい。そこでさらに空の分割代入を行う。

const key = 'z';

const { [key]: {}, ...p2 } = p1; // {}の代わりに[]でも可

// 不要な定数は定義されない!


これでよし!と思いきや、keyのプロパティが存在しなかったり、値がundefinedだった場合にエラーが起きてしまう。

const p1 = {
  x: 0,
  y: 1,
};

const key = 'z';

const { [key]: {}, ...p2 } = p1; // TypeError: p1[(intermediate value)] is undefined


ということでデフォルト値を指定する。

const p1 = {
  x: 0,
  y: 1,
};

const key = 'z';

const { [key]: {} = {}, ...p2 } = p1; // 問題なし!

これでよし!ただし、プロパティの値がnullの場合はデフォルト値が採用されず、先ほどと同様にTypeErrorが投げられてしまうので、値にnullが含まれ得るオブジェクトには使えない。


そのままreturnする場合などは、IIFEで引数をdestructuringすれば1文で書けるけど、ここまでくるとさすがにやりすぎな感じがする。

switch (action.type) {
  case 'PAGE_CLOSE':
    return (({ [action.id]: {} = {}, ...nextState }) => nextState)(state);

  // ...
}
改めて考えると、IIFEを使う場合はローカル変数のことを気にする必要はないから、_で問題なかった。
switch (action.type) {
  case 'PAGE_CLOSE':
    return (({ [action.id]: _, ...nextState }) => nextState)(state);

  // ...
}

書き捨てではない・読まれるコードでは、lodash/fpomitなどを使おう……。


TypeScriptでもちゃんと型がomitされて幸せ。


とはいえもう型のomitも表現できるようになってるけど。

TypeScriptでReduxのActionを書くときの煩わしさを軽減するためのモジュールを作った

TypeScriptで型の恩恵を受けられるようにReduxアプリケーションのコードを書こうとすると、同じようなことを繰り返し書かなければならないところがある。顕著なのはAction周りだと思う。同じことを思う人が多いのか、既に楽に書くためのモジュールがたくさんある(ts-action, typescript-fsa, typesafe-actions, などなど)けど、Middlewareが必要だったり、Reducerの書き方を変えなければならなかったりしてどれもしっくりこない。

もっとシンプルに解決できないかなと思ってモジュールを作ってみた。実装としてはTypeScriptで14行、提供するのは2つのAPIのみ。

以下、Redux Async Exampleの例を借りて、pureにTypeScriptで書いたコードとこのモジュールを使って書いたコードを比較してみる。

続きを読む

ホイール操作でスライドをめくるユーザースクリプト

以前作ったslideshare scrollというユーザースクリプトQiitaのスライドにも対応しようと思ったものの、3年前のコードで無駄が多いので一から書き直した。

createWheelHandlerで前後ボタンの取得を遅延させているのは、Qiitaのスライドが非同期で組み立てられるため。

GitHub上のJSコードでモジュールをリンクで辿れるようにするユーザースクリプト

OctoLinkerという、より高機能な拡張機能が既に存在した。

を作った。GitHub上でのコードリーディングが少し楽になる(じっくり読むならgit clone)。


続きを読む

JavaScriptでgetterとsetterのどちらか一方のみをオーバーライドする

JavaScript でオブジェクトに accessor property を定義したとき、継承先でそのプロパティの getter, setter のどちらか一方のみを override するのは一筋縄にはいかない。

例として、長方形を表す Rectangle と、正方形を表す Square を定義することを考える。SquareRectangle を継承する。

続きを読む

switch文によるコールバック地獄回避

url.txtに書かれているURLを取得し、そのURLのリソースの内容を取得し、1秒後にその内容をalertするという例(のための例)を考える。

まずは普通に書いてみる。

var client = new XMLHttpRequest;
client.open('GET', 'url.txt');
client.onload = function (event) {
    var url = client.response;
    client.open('GET', url);
    client.onload = function (event) {
        setTimeout(function () {
            alert(client.response);
        }, 1000);
    };
    client.send(null);
};
client.send(null);

次にswitch文を使って書いてみる。

var step = 0;
var client;
(function callback(arg) {
    switch (step++) {
    case 0: client = new XMLHttpRequest;
            client.open('GET', 'url.txt');
            client.onload = callback;
            client.send(null);
    break;  
    case 1: var url = client.response;
            client.open('GET', url);
            client.onload = callback;
            client.send(null);
    break;  
    case 2: setTimeout(callback, 1000);
    break;
    case 3: alert(client.response);
    }
}());

おまじないのswitch文によってベースとなるインデントが深くなるものの、メインとなるコードは縦に揃う。コードの流れもopensendopensendsetTimeoutとなり直観的。

……Duff's deviceのコードを見て思いついただけ。

JavaScriptでカスラムエラーをどう作るか

はじめに:IE 11、Firefox 28、Chrome 34で試している

JavaScriptのErrorオブジェクトにはstackプロパティにコールスタックを表す文字列がセットされる。これは現行のECMAScript仕様では規定されておらず、実装によって違いがある。

function foo() { bar(); }
function bar() { baz(); }
function baz() { throw new Error('X'); }

(function main() {
    try {
        foo();
    } catch (error) {
        console.log(error.stack);
    }
})();

Chromeでのコンソール出力の例。

Error: X
    at baz (http://localhost/CustomError.html:8:24)
    at bar (http://localhost/CustomError.html:7:18)
    at foo (http://localhost/CustomError.html:6:18)
    at main (http://localhost/CustomError.html:12:9)
    at http://localhost/CustomError.html:16:3

このように、ネイティブに用意されているエラーオブジェクト用のコンストラクタ(Error, TypeError, RangeErrorなど)を使う場合、stackプロパティはどの実装でもセットされる。ただし、セットされるタイミングは少し異なり、FirefoxChromeではオブジェクト作成時にセットされ、IEではthrowされるときにセットされる。

var e = new Error('X');
console.log(e.stack); // IEではまだセットされていない
try {
    throw e;
} catch (e) {
    console.log(e.stack); // セットされる
}

ネイティブに用意されているコンストラクタでは物足りない場合、Errorを継承した独自のエラーコンストラクタを作ることを思いつく。ところが、これだとIEでしかstackプロパティがセットされない。

function TimeoutError(message) {
    Error.call(this, message);
}
TimeoutError.prototype = Object.create(Error.prototype, {
    constructor: Object.getOwnPropertyDescriptor(TimeoutError.prototype, 'constructor'),
    name: { value: 'TimeoutError', configurable: true, enumerable: true, writable: true }
});

var e = new TimeoutError('session timeout');
try {
    throw e;
} catch (e) {
    console.log(e.stack);
    // IEではコールスタックを表す文字列
    // Firefoxでは空文字列
    // Chromeではundefined
}

IEではthrowされるオブジェクトがError.prototypeを継承していればstackプロパティがセットされるようだ。FirefoxChromeはネイティブコンストラクタでのみstackプロパティがセットされるように見える。


ではどうすればいいだろうか。コンストラクタを用意せず、継承もせず、こんなので十分だと思う。

var createError = (function () {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    return function createError(name, message, properties) {
        var e = new Error(message);
        e.name = name;
        if (properties)
            for (var key in properties)
                if (hasOwnProperty.call(properties, key))
                    e[key] = properties[key];
        try {
            throw e; // for IE
        } catch (e) {
            return e;
        }
    };
})();

var timeoutError = createError('TimeoutError', 'session timeout');
var fetchError = createError('FetchError', '404 Not Found', { statusCode: 404 });

エラーオブジェクトを扱うときは、instanceofではなくnameプロパティからその種類を判別すればよい。ネイティブのエラーオブジェクトも同じように判別できる(ECMAScript仕様にて規定)。

new TypeError().name === 'TypeError';

fetch(url, { timeout: 10 * 1000 }).then(function (text) {
    // do something
}, function (error) {
    switch (error.name) {
    
    case 'TimeoutError':
        alert('タイムアウトしました');
        break;
    
    case 'FetchError':
        if (error.statusCode === 404) {
            alert('リソースが見つかりません');
            break;
        }
        alert('リソースを取得できませんでした(' + error.statusCode + ')');
    }
});

createError関数の分、コールスタックを一行消費してしまうのが惜しい。IEChromeでは、stackプロパティのコールスタックは10行までのようだ。

(function () {
  function a() { b(); }
  function b() { c(); }
  function c() { d(); }
  function d() { e(); }
  function e() { f(); }
  function f() { g(); }
  function g() { h(); }
  function h() { i(); }
  function i() { j(); }
  function j() { k(); }
  function k() { l(); }
  function l() { throw new Error('HELLO?'); }

  try {
    a();
  } catch (e) {
    console.log(e.stack);
  }
})();

Chromeでのコンソール出力の例。

Error: HELLO?
    at l (http://localhost/CustomError.html:30:24)
    at k (http://localhost/CustomError.html:29:18)
    at j (http://localhost/CustomError.html:28:18)
    at i (http://localhost/CustomError.html:27:18)
    at h (http://localhost/CustomError.html:26:18)
    at g (http://localhost/CustomError.html:25:18)
    at f (http://localhost/CustomError.html:24:18)
    at e (http://localhost/CustomError.html:23:18)
    at d (http://localhost/CustomError.html:22:18)
    at c (http://localhost/CustomError.html:21:18)

ES6のObject.assignがあれば、それで十分かもしれない。

throw Object.assign(new Error('404 Not Found'), { name: 'FetchError', statusCode: 404 });