「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ に引き続き、TypeScriptの記事を書きました。
TypeScriptに苦手意識を持っている人に向けて再び触るきっかけを作りたいと考えて書いたので、細かい言語仕様にはまったく触れてませんが、「ああこれならやってみてもいいな」と思ってもらえたら幸いです。
「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ に引き続き、TypeScriptの記事を書きました。
TypeScriptに苦手意識を持っている人に向けて再び触るきっかけを作りたいと考えて書いたので、細かい言語仕様にはまったく触れてませんが、「ああこれならやってみてもいいな」と思ってもらえたら幸いです。
TypeScriptのコンパイラ・プラグインとして振る舞いASTの操作を行えるcustom transformer (AST preprocessor) が実装されたのは TypeScript 2.4 (2017年) でした。
そのときの様子は次のエントリに非常によくまとまっています。
[TypeScript 2.4] custom transformer を利用して実行時に型情報を参照可能にする - Qiita
さて、現在のTypeScript v3.3 でのTransformer APIの状況はというと、TypeScript compiler本体としては依然としてドキュメントに乗ってない水準の扱いです。特に、コンパイラAPI そのままではtransformerは型情報を利用できません。ts.TransformerFactory
に渡される ts.TransformationContext
に ts.Program
や ts.TypeChecker
が存在しないためです。これは、TypeScriptコンパイラ開発チームによれば意図的な設計ということです。
(あると便利だから再検討してほしい!とコメントはしましたが、望みは薄いでしょう)
つまり、標準的なtransformerができることは、babel pluginとできることに本質的な違いがない、ということで、これではあまり面白くないですね。
しかし先に紹介したエントリではコンパイル時型情報を実行時型情報(=JavaScriptのオブジェクト)に変化しています。コンパイラAPIを駆使すればtransformerが型情報も扱えるということですね。しかしそれはtransformer用にカスタムコンパイラを作ることに等しいため、transformerは開発するのもテストするのも利用するのも難易度が高い状態です。
とはいえ、2019年現在では、有志によって開発されたプラガブルなカスタムコンパイラでtransformerの開発・利用が少しだけ楽になりました。
ts.Program
を与えますts.Program#getTypeChecker()
で ts.TypeChecker
にアクセスできるので、これを通じて型情報を利用できますts.Program
や ts.TypeChecker
を受けとるようにできますts-node
は --typescript
オプションでカスタムコンパイラを設定できるため、 ts-node --typescript=ttypescript
とすると ts-node でも型情報を利用するtransformerを利用できますts-loader
も typescript
オプションで ttypescript
を利用できますこのあたりは先のエントリの作者による ts-transform-keys のREADMEに詳しく載っています。
https://github.com/kimamula/ts-transformer-keys
とはいえ、有志によるカスタムコンパイラに依存しすぎるのもあまり望ましくないので、できれば標準のコンパイラAPIとして型情報へのアクセス方法を解放してほしいところです。
ぱっと考えただけですが、次のようなことができます。
まず、TypeScriptの型情報を React の prop types に変換できるはずです。通常であればTypeScriptの型情報はコンパイル時に失われるので次のような typeToPropTypes<T>()
は実装しようがないのですが、transformerがこの関数呼び出しをprop typesに置き換えてしまえばいいというわけです(まったく試していませんが、このアイデアを実装したtransformerはすでにあるようです https://github.com/joelday/ts-proptypes-transformer )。
// これはイメージです interface Props { /* ... */ } cass SomeComponent extends React.Component<Props> { static redonly propTypes = typeToPropTypes<Props>(); }
次に、TypeScriptの型情報をGraphQLのクエリ(クエリフラグメント)に変換できるのではないかと考えています。
// これはイメージです interface Tweet { author: { id }; content: string; tweetedAt: Date; } const GetTweet = gql` query { tweets { edges { // { author { id }; content; tweetedAt } に変換する ...${typeToQueryFragment<Tweet>()} } } } `;
というわけで、TypeScriptの型情報を利用したtransformerを安定して開発・利用できるようになるといろいろ夢がありますね。ただし、繰り返しになりますが、現在はその利用のためにTypeScriptのカスタムコンパイラを作る必要があります。プロダクションに入れる際は自己責任でどうぞ。
表題のとおりで、fetch(...)
はエントリが存在しないと例外を投げるため、エントリが存在しないときに nil
を返すアクセサよりもtypoに対して安全です。
こういうのは rubocop-rails でカバーできるといいなと思って起票して、良さそうというコメントはもらったんですが:
https://github.com/rubocop-hq/rubocop-rails/issues/42
いかんせん rubocop-hq/rubocop-rails 自体がまだ開発中なのでいつ使えるかわかりません。
こういうときにquerlyを使うといいですね。
(cf. プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利 - Islands in the byte stream)
というわけでruleはこんな感じです。
rules: - id: kibela.prefer_fetch_for_rails_secrets pattern: subject: "Rails.application.'credentials.'key()" where: key: '/.+/' credentials: '/(?:secrets|credentials)/' message: "`Rails.application.secrets.fetch(...)` を使いましょう。" before: - "Rails.application.secrets.foo" - "Rails.application.credentials.foo" after: - "Rails.application.secrets.fetch(:foo)" - "Rails.application.credentials.fetch(:foo)"
ないと困るので、 polyfillをつくりました。MediaQueryList
すら実体がないので、やむをえず mathMedia()
を上書きしています。
// MediaQueryList.prototype.addEventListener.ts if (typeof matchMedia !== "undefined" && !matchMedia("all").addEventListener) { console.log('installing polyfill: MediaQueryList.prototype.addEventListener'); const originalMatchMedia = matchMedia; self.matchMedia = function matchMedia(mediaQuery: string): MediaQueryList { const mql = originalMatchMedia(mediaQuery); mql.addEventListener = function (eventName: "change", listener: (event: MediaQueryListEvent) => void) { // tslint:disable-next-line this.addListener(listener); }; return mql; }; }
なぜないかというと、このAPIを定めたCSSOMで、最初は addListener
というメソッドでイベントリスナを登録していたのが、途中からDOM標準の EventTarget
interface を実装するものとして addEventListener
を利用可能にしたから、ということのようです。
https://drafts.csswg.org/cssom-view/#mediaquerylist
Note: This specification initially had a custom callback mechanism with addListener() and removeListener(), and the callback was invoked with the associated media query list as argument. Now the normal event mechanism is used instead. For backwards compatibility, the addListener() and removeListener() methods are basically aliases for addEventListener() and removeEventListener(), respectively, and the change event masquerades as a MediaQueryList.
こういう経緯で TypeScript の lib.dom.d.ts
では addListener
がdeprecated扱いではあるものの、素直に addEventListener
を使えない、と。こういうのはpolyfillするのがいいですね。npmにもありそうですが、実装するのは簡単なので自前実装ということにしました。
追記: この件に関してエンジニアHubにもTypeScriptの記事を書きました: TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に - エンジニアHub|Webエンジニアのキャリアを考える!
TypeScriptに対する失望は2パターンあって、その理由は理解できるのですが、いずれにせよそこでTypeScriptを捨てる判断をするのはもったいないと思っています。この2つの失望を感じたとしてもなお、TypeScriptには導入する価値があると思っています。
そこらのブログやTwitterで観測していると、理由の7割くらいこれです。これは、TypeScriptが独立した言語ではなくJavaScriptへのトランスパイラ(言語変換ツール)であり、独立したランタイムを持たないことに由来します。
TypeScriptはJavaScriptの上位互換(=すべてのJavaScriptは、構文的にはTypeScriptとして正しく、コンパイルが通ればほぼJavaScript同じ振る舞いをする)なので、JavaScriptのいまいちイケてない言語機能をそのまま継承してしています。
なので、TypeScriptを単体の言語として評価するとありえないレベルのイケてない機能がたくさんありますが、それはTypeScriptではなくJavaScriptの欠点であることがほとんどです。
これについては仕組み上どうしようもないので、JavaScriptを学んでJavaScriptの罠にはまらないようにするしかありません。
"noImplicitAny": true
がつらくて失望する "noImplicitAny": true
というのは、暗黙の any
を禁止するオプションで、tsconfig.json に設定します。
これをtrueにすると、型推論の効かない箇所(関数の引数など)で型アノテーションが必要になります。また、ライブラリのimport時にそのライブラリが型情報(.d.ts)を提供していることを要求します。
この「ライブラリに型情報を要求する」というのが制約として非常に強いため、チームの開発者全員がTypeScriptのエキスパートでないかぎりは、型付けに際して消耗を強いられます。そして消耗を強いられたメンバーは「TypeScriptを導入したらDXが下がった」と感じる可能性が高いでしょう。
私は "noImplicitAny": false
を奨励しています。型付けに振り回されないように、がんばらないTypeScriptでいいと思っています。それでもJavaScriptに比べれば静的にチェックできる範囲がずっと広いので、DXの向上やアプリケーションの安定した運用には十分役にたちます。
アプリケーションコードではではなるべく型をつけたほうがいいので、ライブラリの型情報を要求しない弱い "noImplicitAny": true
があればいいんですけどね。
最近は tsc --init
で生成する tsconfig.json が "noImplicitAny": true
をデフォルトにしていますが、強い気持ちでこれをfalseにして「がんばらないTypeScript」を実践することをおすすめします。
ほかに代替がないし、考えられもしないので、生き残るでしょうね。
単体の言語としてはもっとまともなaltJSはすでにいくつかあります。しかし、JSがブラウザで動作する唯一の高級言語(低級言語としてはWASMがあるので)である限りは、それの限りなく薄いラッパーであるTypeScriptは価値を持ちしつづけるでしょう。
もっとも、発展的解消として別の名前を与えられて規格化する未来はあると思います。たとえば、SwaggerがOpenAPIとして規格化されたように。
書きました*1。
GraphQLという規格そのものについての解説が前半で、後半はRailsアプリのWeb APIをRESTful APIからGraphQL APIに書き換えるというハンズオンです。
GraphQLの特徴やら Swagger, gRPC, etc との比較はわりとよく見かけるので、そのあたりについては最低限触れるにとどめて、GraphQLという規格について知るための入門記事として書きました。特にRelay (Relay Server Specification) についてはあまり日本語の説明を見ないので、この記事できちんと紹介できてよかったなと思っています。
サンプルコードは https://github.com/gfx/graphql-blog にあります*2。
この記事は結構長いんですが、それでも発展的な使い方に触れる余裕はあまりありませんでした。このあたりはまたどこかでフォローできたらと思います。
この記事で触れていないトピックについては、私を含めた数名が書いている次のscrapboxに少しあったりはします。 ただしこのscrapboxは graphql-ruby 専用です。
追記: 加筆修正してGitHub projectにしました: https://github.com/gfx/android-oss-best-practices
(potatotips #56 (iOS/Android開発Tips共有会) - connpass)
久しぶりのpotatotipsでのLTでした。
「OSS開発のリテラシー」は他の言語版もほしいですね。誰か書いてくれないかな〜|д゚)チラッ
追記: Apollo Boost は Apollo Client に統合される見込みのようです。Apollo Client 3.0 Roadmap · Issue #33 · apollographql/apollo-feature-requests · GitHub
TypeScript用のGraphQL clientであるところの Apollo ですが、これのGet Startedなどのドキュメントでは apollo-boost
というパッケージを使っています。
しかし、 apollo-boost
はサンプルコードをシンプルにするための apollo-client
などのラッパーと考えたほうがよく、プロダクションコードでは apollo-boost
ではなく apollo-client
および関連モジュールを直接使うほうがいいです。
以下理由:
apollo-boost
は "zero-config" な apollo-client
のラッパーだが、カスタマイズ性に乏しい
apollo-boost
を使い、カスタマイズが必要になったら apollo-client
を使え、という位置づけのようapollo-boost
から apollo-client
への移行ガイドもある:Apollo Boost is a great way to get started with Apollo Client quickly, but there are some advanced features it doesn’t support out of the box.
apollo-client
の強力な機能である "link" (Rack middleware のようなもの) をカスタマイズできないので、バッチリクエストや GraphQL subscriptions を利用する方法がないapollo-client
を生で使うほうがよいimport ApolloClient from "apollo-boost"
と import { ApolloClient } from "apollo-client"
の2パターンがあるが、この2つの ApolloClient は(継承関係にあるものの)コンストラクタのオプションが全く違うので別物と考えたほうがよく、非常に混乱する特に、 apollo を使うとすぐ batch request をしたくなるでしょうから、プロダクションコードでは最初から apollo-boost
を使わないで apollo-client
を使うほうが移行する手間が省けるというものです。
追記: Terser v3.10.11 でこの問題が修正されていることを確認しました。現在は collapse_vars: false
というワークアラウンドは不要になりました。
webpackのproduction buildの話です。
Reactが悪いわけではないんですが、たまたま秘孔をつくコードがReactないし関連するReact v16に依存するライブラリにあったんでしょうね。
collapse_vars: false
にすることで問題は回避できるそうで、試してみるとそのとおりだったというわけで webpack.config.js の optimization の設定はこんな感じにしました。
if (production) config.optimization = { minimizer: [ new TerserPlugin({ terserOptions: { sourceMap: true, parallel: true, compress: { collapse_vars: false, // workaround for a minifier's bug: https://github.com/terser-js/terser/issues/120 drop_console: true, }, }, }), ], noEmitOnErrors: true, }; // ... }
Sider社の開発する Querly を使うと「歴史的経緯の説明」をコード化できるよという話です。
Querlyの話は以前も書いたことがあります: プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利 - Islands in the byte stream
Querlyは便利で可愛いやつなんですが「価値がわかる」ところに到達するのが難しいツールなので(ぼくなんて2年かかってようやく理解できましたからね!)、もうちょい公式ドキュメントがわかりやすくなるといいな〜と思いますね。あと querly init
がほしい。