お詫び
当初、 TypeScript 2.3 の機能として紹介していましたが、正しくは 2.4 でした。
誤った情報を掲載してしまい、申し訳ありません。
TypeScript 2.3.1 では、 API として custom transformer を指定できるようにはなっているものの、 bug fix が取り込まれておらず、正常には使えない状態です。
以下、本文
前回の記事に引き続き、 TypeScript 2.4 で導入される custom transformer について紹介します。
custom transformer の概要については、前回の記事を参照してください。
前回は、最初なので直感的に分かりやすいものを作りましたが、今回はより個人的な需要の高いものを作りました。
成果物
github: https://github.com/kimamula/ts-transformer-keys
npm: https://www.npmjs.com/package/ts-transformer-keys
お題
以下のような signature の関数を実現します。
export declare function keys<T extends object>(): Array<keyof T>;
こんな風に使います。
import { keys } from 'ts-transformer-keys';
interface Props {
id: string;
name: string;
age: number;
}
const keysOfProps = keys<Props>();
console.log(keysOfProps); // ['id', 'name', 'age']
コンパイルの際に、 ts-transformer-keys/transformer
で export している関数を custom transformer として使うと、上記のコードが以下のような JavaScript にコンパイルされます(custom transformer の使い方は前回の記事を参照してください)。
var ts_transformer_keys_1 = require("ts-transformer-keys");
var keysOfProps = ["id", "name", "age"];
console.log(keysOfProps); // ['id', 'name', 'age']
では次に、これを使ってどのように React のバケツリレー問題を解決できるか見ていきます。
React のバケツリレー問題の解決策
React のバケツリレー問題そのものについては、ググればいくらでも情報が出てくるので、ここでは詳しく説明しません。
筆者の知る限り、この問題の解決策には主に以下のようなものがあります。
1. container component を導入し、バケツリレーの発生範囲を狭める
Redux で採用されている方法です。
参考: http://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components
Redux が好きな人はこれに乗っかってしまえばよいでしょう。
TypeScript ユーザーの筆者としては、この方法は React の context という、 type safe になりえない部分に依存することになるため、あまり好きになれません。
2. 子コンポーネントを props.children
に受け渡すようにする
執筆時点で "React バケツリレー" でググると以下のページが一件目にヒットしますが、ここで紹介されている方法です。
この方法は確かに綺麗ですが、常に使えるわけではないでしょう。
個人的には、 props.children
を使うかどうかは、「バケツリレーがつらいかどうか」ではなく、コンポーネントとしてのあるべき単位や責務の境界を考えた上で決定すべきことのように思えます。
3. props を丸ごと子コンポーネントに受け渡す
子コンポーネントで必要な props を一つずつ指定するのではなく、丸ごと渡してしまうやり方です。
例: http://mae.chab.in/archives/2897
この方法の問題点は、 shouldComponentUpdate
の実装がつらくなることです。
コンポーネントが必要な props だけ持っている場合、通常は PureComponent
を継承してしまえば shouldComponentUpdate
の実装は不要です。
PureComponent
では、 props の内容を shallow に比較する shouldComponentUpdate
がすでに実装されているからです。
しかし、 props を丸ごと渡してしまう場合、このやり方は通用しなくなります。
props には、そのコンポーネントの関心のない情報も含まれ、それを単純に比較してしまうと、関係のない情報が更新された場合でも、 shouldComponentUpdate
が true
を返してしまうからです。
したがって、 props の中身を一つ一つ比較する shouldComponentUpdate
を、自前で実装することになります。
shouldComponentUpdate(nextProps: Props) {
return nextProps.myProp1 !== this.props.myProp1
|| nextProps.myProp2 !== this.props.myProp2
// このコンポーネントで直接使っていない、子コンポーネントの props に対しても判定が必要
|| nextProps.childProp1 !== this.props.childProp1
|| nextProps.childProp2 !== this.props.childProp2
// ...
}
これでは、 props の指定のつらさが、 shouldComponentUpdate
の実装のつらさに変わったに過ぎません。
shouldComponentUpdate
の実装では、比較すべきものを比較し忘れてもコンパイルエラーになったりはしないため、 TypeScript ユーザー的にはむしろ状況は悪化しているとすら言えるかもしれません。
作成した custom transformer を使ったバケツリレー問題の解決
今回作成した custom transformer を使うと、上記 3 の shouldComponentUpdate
の問題を以下のように解決できます。
shouldComponentUpdate(nextProps: Props) {
return keys<Props>().some(key => nextProps[key] !== this.props[key]);
}
いかがでしょうか?
我々はただ、それぞれのコンポーネントが必要とする props を正しく型として定義しさえすれば、実処理上において props の中身一つ一つを列挙して何やかやする必要はなくなるというわけです。
終わりに
今回作ったものは、 TypeScript でコンパイラを拡張できるようになりそうな気配を感じた時から、できるようになったらきっと作ろうと思っていたものでした。
TypeScript 2.4 で、ここで書いたような機能が普通に使えるようになる日がくるのが楽しみです。