きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

protoc-gen-xxxx のオプションを確認する方法を探す

protoc を使いサーバ/クライアント実装の組み合わせや変換プロキシなどのエコシステムを色々と検証しているのですが、そのせいもあって手元にある分だけでも 7 つ程の protoc-gen-xxxx プラグインがインストールされています。

$ protoc-gen-
protoc-gen-dart          protoc-gen-grpc-gateway  protoc-gen-ts
protoc-gen-go            protoc-gen-grpc-web      
protoc-gen-govalidators  protoc-gen-swagger

これらがあることで

protoc -I. \
  --grpc-gateway_out=logtostderr=true:. \
  my.proto

のようにデフォルトではサポートされていない --xxxx_out オプションを使いコードを生成することができるのですが、そのオプションで実行される protoc-gen-xxxx の更にそのオプションである --xxxx_out="<plugin-options>:out" の種類がプラグインごとに様々だったり、確認方法が提供されているかもまちまちだったりするのでちょっと整理してみたいです。

protoc-gen-xxxx はコマンドとして実行できる

まず基本的なところの確認です。

protoc-gen-xxxxprotoc で使うためにはパスを通してコマンドとして実行できるようにしておく必要があります。 なのでそのプラグインが使えている状態であれば、protoc-gen-xxxx は単体のコマンドとしても実行できます。

標準入力を受け取り動作するようなので適当に dummy-contents と入力して Ctrl+d で EOF を送ると「proto として読めないよ」という旨のようなエラーが返されます。

$ protoc-gen-grpc-gateway
dummy-contents
F0922 13:33:41.840980   97540 main.go:45] failed to unmarshal code generator request: proto: can't skip unknown wire type 4

という、基本的なインタフェースとしては protocprotoc-gen-xxxx は標準入出力を介して繋がっているだけなので、割とシンプルで分かりやすいです。

コマンドライン引数でオプションを渡してヘルプを見る

--xxxx_out="<plugin-options>:out" として protoc に渡しているオプションはコマンドラインからプラグインを実行する際にはコマンドライン引数として渡すことができるようです。

protoc-gen-grpc-gateway で試してみると以下のようにヘルプが表示されます。

$ protoc-gen-grpc-gateway -h
Usage of protoc-gen-grpc-gateway:
  -allow_delete_body
        unless set, HTTP DELETE methods may not have a body
  -allow_repeated_fields_in_body body
        allows to use repeated field in body and `response_body` field of `google.api.http` annotation option
:  -logtostderr
        log to standard error instead of files
:
  -v value
        log level for V logs

この -h でヘルプが見られるのは protoc-gen-grpc-gatewayコマンドラインオプションの実装に flag というパッケージを使っているからです。

ついでに golang/glog というロギングパッケージのオプションも同様に flag で実装され、そのオプションも protoc-gen-grpc-gateway のオプションとして露出しているようなので、ログを標準入力に出す -logtostderr や粒度を設定する -v (verbose) オプションも利用できます。こういうオプションが備わっているプラグインだとデバッグなんかも楽になりますね。

と言ってもこの方法が使えるのは手元のプラグインの中で言えば protoc-gen-grpc-gateway 系だけみたいです。

--grpc-gateway_out=logtostderr=true,v=1:out

のように verbose レベルを指定できます。

諦めてコードを読む

オプションにヘルプが用意されているプラグインはいいですが、大部分のプラグインには用意されていないようなのでその場合はドキュメンテーション頼りか、コードを読んで対応する場合が多いです。自分が知らないだけでプラグインのオプションを確認する方法があるのかもしれませんが...

大抵 GitHubリポジトリoption や存在することが分かっているオプション名などのキーワードで検索すれば引っかかってきます。例えば protoc-gen-go の場合は ここ に見つかりました。

この辺は protoc で色々生成しているうちに妙に探すのが小慣れてきた感じがします。この方法の難点は、探し出したオプションが将来的に消えることはないのだろうか、と心配になるところですね。

TypeScript の型定義を観点に gRPC-Web の実装ライブラリを比較する

gRPC-Web の実装を調べてみると、どうやら複数の実装があるみたいです。 それぞれ関連ライブラリの組み合わせが決まっており、きちんと区別しないと混乱しそうなのでまとめてみます。

比較のために作成したプロジェクトは以下に置いてあります。

gRPC-Web の実装を簡易比較

  • grpc/grpc-web (star: 700)
    • grpc のオーガニゼーション(以下本家)にある...と思ってこれを選ぶとまだ成熟しておらず後悔する
    • 特に TypeScript の型定義は完全にサポートされていない
      • アクセッサの戻り型が {} で潰されていたりする
  • improbable-eng/grpc-web (star: 1.7k)
    • 本家よりも勢いがあって利用者も多そう
    • TypeScript の型定義は本家よりも充実している

なので gRPC の本家本元のリポジトリにも grpc/grpc-web という実装はあるけれど、 improbable-eng/grpc-web の方が人気があり実装状況もマルという状況ですね。

以下、両者を細かく見ていきます。 比較の題材にした Protobuf 定義は以下の通りです。

syntax = "proto3";

package hello;

service Greeting {
  rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
  string message = 1;
}

message HelloResponse {
  string message = 1;
  Profile profile = 2;
}

message Profile {
  string name = 1;
}

grpc/grpc-web

本家お膝元のリポジトリです。 しかしもう一つのリポジトリと比較すると実装に開きがあります。

protoc \
  --js_out=import_style=commonjs:${OUT_DIR} \
  --grpc-web_out=import_style=commonjs,mode=grpcwebtext:${OUT_DIR} \
  hello.proto

のように生成します。 これで以下のようなファイルが生成されます。

out/grpc-web-by-grpc/proto
├── hello_grpc_web_pb.d.ts
├── hello_grpc_web_pb.js
├── hello_pb.d.ts
└── hello_pb.js

上で書いた「TypeScript の型が {} で潰される」とは以下のように Profile 型が返ってきてほしいところが {} になっているところを指しています。

export class HelloResponse {
  constructor ();
  getMessage(): string;
  setMessage(a: string): void;
  getProfile(): {};
  setProfile(a: {}): void;
  serializeBinary(): Uint8Array;
  static deserializeBinary: (bytes: {}) => HelloResponse;
}

おそらくまだ実装途中なのだと思います。 toObject の型が定義されていないのも物足りないところですね。

improbable-eng/grpc-web

本家よりも勢いのあるリポジトリです。

protoc \
    --js_out="import_style=commonjs,binary:${OUT_DIR}" \
    --ts_out="service=true:${OUT_DIR}" \
    hello.proto

のように生成します。これで以下のようなファイルが生成されます。

out/grpc-web-by-improbable-eng/proto
├── hello_pb.d.ts
├── hello_pb.js
├── hello_pb_service.d.ts
└── hello_pb_service.js

微妙にファイルの命名が違っていますが、hello_pb.js の型付けのために hello_pb.d.ts が生成されているのは同じですね。 ただこちらの方が Message の型付け度合いは以下のように充実しています。

export class HelloResponse extends jspb.Message {
  getMessage(): string;
  setMessage(value: string): void;

  hasProfile(): boolean;
  clearProfile(): void;
  getProfile(): Profile | undefined;
  setProfile(value?: Profile): void;

  serializeBinary(): Uint8Array;
  toObject(includeInstance?: boolean): HelloResponse.AsObject;
  static toObject(includeInstance: boolean, msg: HelloResponse): HelloResponse.AsObject;
  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
  static serializeBinaryToWriter(message: HelloResponse, writer: jspb.BinaryWriter): void;
  static deserializeBinary(bytes: Uint8Array): HelloResponse;
  static deserializeBinaryFromReader(message: HelloResponse, reader: jspb.BinaryReader): HelloResponse;
}

toObject にも型が設定されているのが嬉しいですね。現状で選ぶならこちら一択だと思います。

protoc のオプション js_out の提供元

余談的なものになりますが、ts-protoc-gen や protoc-gen-grpc-web を比較しているときに両者が共通して利用している --js_out が気になりました。 protoc のデフォルトのオプションに含まれているので、元から含まれているものでしょうか。 となると JavaScript のコードジェネレータの実装も protoc に含まれているということなのでしょうか。

https://github.com/protocolbuffers/protobuf/tree/master/js がそれっぽいですね。

ただ gRPC のコードは含まれておらず単純にバイナリと JavaScript のデータの変換処理のみが行われているようです。 具体的には Protobuf の Message ごとに以下のメソッドを生やしているようです。

  • toObject
  • deserializeBinary
  • deserializeBinaryFromReader
  • serializeBinary
  • serializeBinaryToWriter

これ以外の処理は生成されないようなので完全にデータ変換のみの実装ですね。 なので protoc に含まれている JavaScript のコードジェネレータで生成されない gRPC 関係の service/server/client などの実装は ts-protoc-gen や protoc-gen-grpc-web などが独自に生成している、という理解をしました。

配列型のJSONから gRPC(protobuf) 用のモックデータを読み込む

gRPC のハンズオンがてら適当なサーバを書いてみて、ある程度ハンドラが充実してきたので今度は適当なデータをJSONで用意して読み込んでサーバで返すようにしよう、としたときに意外と大変だったという記録です。

動作するサーバコードは以下のリポジトリに置いてあります。

色々試したところそもそもやっていることの筋が悪いような気がしたのですが、一応動いたは動いたので記録として残しておきます。

サーバ・データ定義

サービス定義

サービス定義は以下のようなよくある ToDo アプリです。

syntax = "proto3";

import "google/protobuf/timestamp.proto";

service ReadOnlyTodo {
  rpc GetTodos(GetTodosRequest) returns (GetTodosResponse) {}
}

message Todo {
  int32 id = 1;
  string title = 2;
  google.protobuf.Timestamp deadline = 3;
}

message GetTodosRequest {
}

message GetTodosResponse {
  repeated Todo todos = 1;
}

サーバ定義

サーバ定義は以下のようにしています。

type todoServer struct {
    todos []*pb.Todo
    mu    sync.Mutex
}

今回はこの todosJSONのデータをロードするための関数を生やそうと試行錯誤しています。ロードするJSONは以下のようなものです。

[
  {
    "id": 1,
    "title": "First ToDo",
    "deadline": "2018-09-01T12:34:56.789Z"
  },
  {
    "id": 2,
    "title": "Second ToDo",
    "deadline": "2018-09-01T12:34:56.789Z"
  }
]

配列形式で複数のエントリが表されています。[ptypes]

試したパターン

実際に試した順番からはちょうど逆転しますが成功したパターンからご紹介します。

成功したパターン

まずは成功したパターンです。

golang/protobuf#675 のコメントを参考にして書きました。 普段の json.Unmarshal と比較するととても複雑なことをしているように見えます。

func (s *todoServer) loadTodos() {

    jsonBytes, _ := ioutil.ReadFile(dataFile)
    jsonString := string(jsonBytes)
    jsonDecoder := json.NewDecoder(strings.NewReader(jsonString))

    // read open bracket
    if _, err := jsonDecoder.Token(); err != nil {
        log.Fatal(err)
    }

    for jsonDecoder.More() {
        todo := pb.Todo{}
        if err := jsonpb.UnmarshalNext(jsonDecoder, &todo); err != nil {
            log.Fatal(err)
        }
        s.todos = append(s.todos, &todo)
    }
}

Unmarshal に標準の encoding/json ではなく protobuf/jsonpb というパッケージを利用しています。 叩いたところ無事データが返ってきました。

$ grpc_cli call localhost:10000 GetTodos ""
connecting to localhost:10000
todos {
  id: 1
  title: "First ToDo"
  deadline {
    seconds: 1535805296
    nanos: 789000000
  }
}
todos {
  id: 2
  title: "Second ToDo"
  deadline {
    seconds: 1535805296
    nanos: 789000000
  }
}

直感的に jsonpb を使った場合

いちいち UnmarshalNext などせずに配列型を直接データの格納先に指定してやればいいのではと思い試してみました。

func (s *todoServer) loadTodos_byInstinctiveJSONPBUsage() {

    reader, _ := os.Open(dataFile)

    if err := jsonpb.Unmarshal(reader, &s.todos); err != nil {
        log.Fatal(err)
    }
}

これを実行すると...というよりコンパイルの時点でエラーが出て

*[]*todo.Todo does not implement "github.com/golang/protobuf/proto".Message (missing ProtoMessage method)

と言われます。つまり pb.TodoMessage の interface を満たしているけれど []pb.Todo には ProtoMessage のメソッドが存在しないので jsonpb.Unmarshalシグネチャ違反ですよと言われているんですね。

標準の encoding/json パッケージを利用した場合

そもそも jsonpb を利用せずに json を利用したらどうなるかです。

func (s *todoServer) loadTodos_byStandardJSONPackage() {

    jsonBytes, _ := ioutil.ReadFile(dataFile)

    if err := json.Unmarshal(jsonBytes, &s.todos); err != nil {
        log.Fatal(err)
    }
}

これを実行すると...

json: cannot unmarshal string into Go struct field Todo.deadline of type timestamp.Timestamp

と返ってきます。string 型を timestamp.Timestamp 型に Unmarshal できないと言っていますね。timestamp.Timestamp 型は Protocol Buffer の ptype (もしくは well-known type) です。

Go 自体そこまで詳しくはないのですが、Unmarshal の振る舞いを変えられたら json でもなんとかなる...んでしょうか。

Go の gRPC のシンプルな Interceptor を自作して理解を深める

Go の gPRC のシンプルな Interceptor を適当に書いたら割とすんなり動いたので基本的な部分をまとめていきます。

今回のサンプルは以下のリポジトリに置いてあります。

go-grpc-middleware にロガーなど様々なミドルウェア(= Interceptor)が用意されていますし、本番用途であればそれらメンテナンスや検証の行き届いたパッケージを使うべきだと思いますが、初見のパッケージのオプションを眺めるのも面倒なのでロガー程度ならスクラッチできないかなと思ってやってみたら意外と簡単でした。基本的に入出力さえ辻褄を合わせておけば何をやるのも自由そうです。

動機としてはもう一つ、grpcフレームワーク的なインタフェースに興味があったというのもありました。既製品のミドルウェアを使う前に基本的なインタフェースだけでも押さえておくと、開発の自由度も上がるかもしれないという目論見です。

それでは以下、今回行ったことの簡単な解説です。

Interceptor を自作して grpc.Server に組み込んで実行する

Interceptor を定義する

今回はミドルウェアとしては定番のリクエストのログを出力する Interceptor を題材にしました。Interceptor について調べる前はやっつけでメソッドごとの各ハンドラに log.Printf を突っ込んで回る作業をしていましたが、それらのコードも一掃できました。

Interceptor は func として定義します。Interceptor には種類があり、今回実装したのは grpc.UnaryServerInterceptor という stream でない値を返す際に動作する Interceptor です。

grpc.UnaryServerInterceptorシグネチャに従い、メソッド名・リクエスト・レスポンスを log として表示する Interceptor は以下のようになりました。引数のシグネチャが長いですがミドルウェア的な処理としては必要な情報が並んでいるだけですね。

func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {

    // ハンドラを実行する
    res, err := handler(ctx, req)

    // リクエスト処理のログを表示する(今回ミドルウェアでやりたかったこと ^-^)
    log.Println("My interceptor called!")
    log.Printf("%s: %v -> %v", info.FullMethod, req, res)

    // 実行結果とエラーを返す(返さないとサーバが結果を返さなくなる ^-^);
    return res, err
}

特に難しいことはやっておらず、関数評価時に渡される reqinfo などの材料をもとに処理を行っているだけです。

一点気になったのが Interceptor 中でメソッドのハンドラを評価し、結果を返さなければならないという点です。そこまでミドルウェアというものを経験したこともないのですが、こういう形式が普通なんでしょうか。Node.js の Express とはお作法が微妙に違うなという感触を得ました。

ちなみに resnil で置き換えたらサーバが空レスポンスを返すようになりました。

grpc.Server に設定する

Interceptor を ServerOption type に変換する grpc.UnaryInterceptor を通して Interceptor をサーバに設定します。

grpcServer := grpc.NewServer(
  grpc.UnaryInterceptor(loggingInterceptor),
)

ここもそこまで難解な見た目にはなっていませんね。シンプルです。

実行

サーバを実行してリクエストを送ったところ、サーバのログが以下のように表示されました。

2018/09/13 22:20:39 gRPC server starts on localhost:10000
2018/09/13 22:20:41 My interceptor called!
2018/09/13 22:20:41 /hello.Greeting/Hello: name:"asa-taka"  -> message:"Hello asa-taka, I am gentle-server"

...というこんな感じで grpc の Interceptor は自作するのも結構簡単なんだな、というお話でした。

余談

開発って

  • その分野のベストプラクティス・定番のライブラリを調べる
  • 調べるのが面倒だからとりあえず手前味噌で実装する

のバランスを探り探りで行なっていく作業だと思うのですが、技術検証フェーズでは後者を自由度高く行えると快適に思えることが多いです。ミドルウェアのインタフェースを知ることも、大いにそれに与するものだと思っております。

Saltstack の highstate や low とは何なのか

最近 Saltstack を触っております。

デバッグがてらに state.show_ 系のコマンドを打つことが多いのですが似たようなコマンドが多くて初見ではちんぷんかんぷんなんですよね。

  • state.show_sls
  • state.show_low_sls
  • state.show_highstate
  • state.show_lowstate

この highlow といった用語はあからさまに Saltstack ドメインの言葉遣いなのですが、ではそれが意味するところとは何なのか、今回はそれを押さえてより理解度深く Slatstack を使えるようになろうと思います。

といっても公式のドキュメントの簡単な和訳や要約なので特に大したことはしてません。

State System Layers

State System Layers に挙げられている項目は上から順に Saltstack における State 系の概念のローレイヤーからのスタックになっているのでそれを順に見ていきたいと思います。

Function Call

  • State System における最もローレイヤーな要素
  • Slatstack における State の実行とは個々の State Function の実行と言える
  • pkg.installed などの関数の実行がこれに当たる

Low Chunk

  • YAML(JSON?) で書かれたデータ構造
  • Salt State Compiler のコンパイル結果がこれ(Low Chunk)の集積物になる
  • Function Call が「実行すること」だとすると Low Chunk は 「実行するとその Function Call が実行されるもの」 と言えそう(ややこしい)

Low State

  • Low Chunk が処理順に並べられたリスト
  • 推測混じりで言えば、ここまでは「どの環境のどのホストで」というものを 考慮しない 純粋なデータ構造・実装や処理のことを指していると思われる*1

High Data

  • SLS Files により構成される YAML として表されるデータ構造
  • SLS Files をまとめたもの、という説明にとどまっている
  • この時点では特に「何のためのもの」というところには触れられていない

SLS

  • SLS レイヤーは High Data の論理レイヤーであると(ちょっと意味が読み取れない)
    • 最終的にデータ構造さえ提供されればその生成過程は問わないとある
  • 用語として SLS Files と SLS Formula は区別したいらしい
    • かつては SLS は単体のファイルで構成されており SLS Formula は SLS Files とも呼ばれていた
    • 現在の SLS は Pillar の内容などが適用され動的に生成されるので SLS Formula と呼ぶのが良い
    • この説明の流れだと SLS Files は個々の *.sls ファイルを指すと考えればいいのだろうか

Highstate

  • SLS を読み込んでどの minion に何を実行させるべきかを判断する

Orchestrate

  • これは Salt の更に上位でやってくれとある

まとめ

ここまで読んで来た内容を今度は逆順におさらいしてみると...

  • Highstate とは各 minion ごとの Low State が集まったもの
  • Low State とは Low Chunk のリストで、Low Chunk とは個々の Function Call を行うためのデータ

と読み取れます。つまり当初気になっていた Slatstack 的な high/low の意味合いとしては

  • 細かい個々の State 管理の項目のことが low
  • 環境やグループなど minion ごとの設定を含めた全体の状態管理が high

...と呼ばれているのかな、という雰囲気はつかめたような気がします。

コマンドの解説を再度読んでみる

以上の前提知識をもとに改めて コマンドリファレンス を読んでみます。

  • state.show_highstate
    • salt master から highstate を取得し表示する
  • state.show_low_sls
    • SLS から Low Data 形式に落とし込まれたものを表示する
  • state.show_lowstate
    • 指定された minion に適用される Low Data を表示する
  • state.show_sls
    • master 上の指定された sls file の State Data を表示する
    • State Data ってなんだろう...

よく見たら high/low の違いと state/sls の違いの組み合わせなんですよね。手元に試せる環境がないので何も言えないのですが、まだ違いがよくわからないんですよね... また気が向いたら今度は触りながら確認していきたいです。

*1:わかりづらい例えをすれば、我々のプロジェクトではこの筆記具のことを「鉛筆」と呼びそれはこういう構造・性質を持っています、ということを言っているのみで、それが運用上どう使われているかはこの段階では触れられていない。

TypeScript の絶対パスの import をプロジェクトルートから辿るようにする

何やら TypeScript では絶対パスでプロジェクトルートから import を行えるようだと耳にしました。これで相対パス地獄から逃れることができますね。早速動作確認していきたいと思います。

使用した TypeScript のバージョンは 3.0.3 です。

動作確認

以下のようなディレクトリ構成で動作確認を進めていきます。

.
├── package.json
├── src
│   ├── app-global-lib.ts
│   ├── index.ts
│   └── nested
│       └── nested
│           └── super-nested-lib.ts
└── tsconfig.json

src/app-global-lib.ts

src 直下の src/app-global-lib.ts からは適当な定数を一つ export します。

export const APP_GLOBAL_CONST = 1

src/nested/nested/super-nested-lib.ts

それを何階層かネストしたファイル src/nested/nested/super-nested-lib.ts から import します。src/... のように絶対パスで指定しています。

import { APP_GLOBAL_CONST } from 'src/app-global-lib'

export const SUPER_NESTED_CONST = 2

このままだと Cannot find module のエラーが出るのですが、ここで tsconfig.json

"baseUrl": "./"

コメントアウトを外すと絶対パスがプロジェクトルートからのパスとして解釈され、コンパイルが通るようになります。

src/index.ts

src/index.js では今まで通り相対パスimport します。node_modules のライブラリも問題なく参照できるか確認したかったので適当に uuidimport しています。

import { APP_GLOBAL_CONST } from './app-global-lib'
import { SUPER_NESTED_CONST } from './nested/nested/super-nested-lib'
import uuid from 'uuid'

console.log(APP_GLOBAL_CONST + SUPER_NESTED_CONST, uuid())

これで無事 1 + 2 で 3 と UUID が表示されました。

感想

とても見やすくなりました。通常の Node.js でもできると嬉しいです。

ところで 以前GitHub - piotrwitek/react-redux-typescript-guide: The complete guide to static typing in "React & Redux" apps using TypeScript を読んだ時に気になったのですが

"baseUrl": "./",
"paths": { "@src/*": ["src/*"] },

という設定を tsconfig.json に行うと import @src/app-global-lib という参照の仕方もできるようなのですが、どちらがオススメなのでしょうか。

参考

react-redux-typescript-guide を読んだ感触

Redux + TypeScript の検証作業で疲弊していたところで react-redux-typescript-guide を読んだら思ったより色々まとまっていて、検証のモチベーションがちょっと高まりました、という感想文です。

現在 Redux + TypeScript でアプリを作ろうと色々調べています。 業務でも趣味でも Redux を使った経験はほとんどありません。 認証情報を格納するためのストアとしてシンプルな reducer と数個の action を定義した程度です。

前回のエントリでは typescript-fsatypesafe-actions についてごく簡単に触った感触について書きました。

dispatch に連動して副作用を実現する redux-observableredux-thunk については何度か手元の試作プロジェクトで軽く触った程度です。 軽く触った程度ですが Redux + Typescript の荒波には十分揉まれました。 分かりやすい進捗が出せなくてそろそろ辛いです。

そして react-redux-typescript-guide というガイドを見つけました。

自分でライブラリを探している途中で何度かページ自体は目にしていたのですが、改めて読んでみたところかなり内容がまとまっているようでした。ここまで荒波にまみれてなんとなく掴めてきた理解を整理する上でも、一旦頭を空にして読む価値のありそうなガイドです。写経や読経は世俗の煩悩にまみれた状態でこそ効力を示します。

このガイドでメインで使われているのは typesafe-actionsredux-observable の組み合わせです。

今回は react-redux-typescript-guide の概要をまとめ...るようなことはせず、どの部分を読んでどういう理解になったか、何が得られたかの、ただの感想文を残していこうと思います。

コード類はほとんど引用していないので読みづらいですが済みません。

Type Definitions & Complementary Libraries

まずは補助的に使うライブラリの紹介です。 typesafe-actions はむしろメインで使うライブラリですね。

React Type Cheatsheet

React + TypeScript のベースとなる型情報です。この辺はある程度 React + TypeScript の経験があれば大体把握しているはずですが、それでも新しく気づいたことがあったので、網羅性というものの価値を感じます。

  • React.CSSPRoperties
    • Material-UI でたまに JSS 用に型定義が欲しくなることがありますが React に含まれていたのは初めて知りました

Component Type Pattern

チートシートの続きで、頻出パターンの逆引き一覧になっています。ここも発見が多いです。実装の段階でお世話になりそうです。

Stateful Components

  • Stateful Components って constructor 無しでもいける
  • defaultProps の例はちょっとわかりづらい(というより読みづらい)

Render Props

Apollo なんかで <Query>{({ loading }) => ... }</Query> とかやるやつですね。 そういえば Render Props と HOC の良し悪しってあまり把握してないんですよね...

Higher-Order Components

そして Higher-Order Components の書き方です。色々理解していないことだらけでした。

  • typeclass の中で定義できる
  • Enhanced なコンポーネントに名前をちゃんとつけててえらい
  • そういえば recompose はこのガイドの中では触れられていないのが気になりました
  • Error Boundary の例もあるのは嬉しいです

以前、FlowApollo を利用していたことがあるので、そこまで苦労しないで読めました。

bindActionCreators

Redux とのつなぎの部分で利用する bindActionCreators (初めて知りました)でワンポイントです。

Redux の bindActionCreators を使うときは props 内で () => void を使うとコンパイルエラーになるけれど () => any で代用可能、という注意書きがあり、このガイドの例もそれで実装されているみたいです。

Redux

そして来ました Redux です。 typesafe-actions を使って action を定義していきますが、 詳しくは The Mighty Tutorial を読んでね、と書いてありますね。

そして 前回 気になったところですが、リンク先にも書いてあります。 大事そうなのでここだけ原文で引用します。

WARNING: When using string constants for action type, please be sure to use simple string literals. Don't use string concatenation, template strings or object map because your type will lose the type information, widening to it's supertype string (this is how TypeScript works).

String Literal を型推論に利用しているので template strings や object map を使うと型が string に落ちてしまい action type の型情報が得られなくなる(= ペイロードの型付けが行えなくなる)ということです。 大変そうですが地道に全ての action の名前をつけていくしかなさそうです。

型付けに別の手法を取っている typescript-fsa にはこういう制約はなさそうです。ここで規模のあるアプリを作成した時に、最終的な使い勝手がどう変わるかが、今後の検証でも気になるところです。

Reducers

State の定義には readonly を使うといいよという話。 ReadonlyArrayutility-typesDeepReadonly も便利そうです。

他は TypeScript のスマートな型解析で if ステートメント内の型がその条件により type narrowing されるという話ですね。

Store Configuration

特に目立ったところはなく、以下の微妙な違いに気をつけてさえすればいいと思います。

  • RootAction は全ての Action の Union 型
  • RootStatecombineReducer された(構造化された) State を示す型

いちいち Root とつけるかは好みですが、実際、プロジェクトを構造化した状態で import を大量に行なっていたりすると、実際は何を参照しているのかあやふやになってくるので、特別な名前で参照できると安心感があります。

他に書かれているのは redux-observable の epic を middleware に仕込むところくらいです。

Async Flow

これは The Mighty Tutorial へのリンクで済まされています。これについてはまたそのうち別途書くかもしれません。

Selectors

これは完全に初見の概念だったのですが、Selector とは Store 上のあるデータに別のデータを適用した結果をキャッシュしておき、その結果が依存する State の変更があるまでそれを保持し続けることを行うライブラリの総称のようです。Computing Derived Data - Redux にも項目があります。

Vue.js の computed に近いといえば近いのでしょうか(あれはテンプレート中で参照するためのものなので用途が違うかもしれませんが)。

Tools & Recipes

個人的に一番嬉しかったところかもしれません。よく利用されるツールの設定例が紹介されています。

  • Common Npm Scripts
  • tsconfig.json
    • @src/... でプロジェクトルート下の ./src を参照するやり方があるのを初めて知りました
      • でもどうやらデフォルトで import src/... が動作するみたいですね...
      • この辺はまた検証してみたいです
    • lib で 何を指定したらいいかの答えはここにありました(毎回迷うんですよね...)
  • Vendor Types Augmentation
    • ライブラリの型定義がおかしい場合に quick fix してやり過ごす方法も載っています

感想

...と、一通り読んできましたが、ただ単に読んだだけで特に手を動かして検証を行なったわけではないので、何ができるようになったではありません。それでも多くの試したいことが発見できたので、技術検証のモチベーションを高める効果はあったと思います。もしくは単に鮮度のいいネタを提供してもらえたと言うべきでしょうか。

とりあえず Redux + TypeScript にかき乱された頭の中は整流されたような気がします。 これでまた型システムの闇に立ち向かえそうな気がします。

気になったところ

ガイドを読んでいて、後で調べたいと思ったところです。

  • HOC と Render Props の比較
    • 両者とも同様にDI的なことを行なっているので、手法の差がどう評価されているのか気になります
  • TypeScript の import path の設定
    • よく ../../../api のように ./src 直下近くに置いたライブラリが参照しづらくなる問題が TypeScript では tsconfig.jsonbaseUrl で解決されているみたいですが、その動作について精査してみたいです
    • import @src/... のパターンと import src/... のパターンがあるみたいですが、どちらが推奨されているのかも気になります