mizdra's blog

ぽよぐらみんぐ

Relay の %future added value, %other との向き合い方

前提

  • まず議論の土台は https://github.com/facebook/relay/issues/2351
    • GraphQL では Enum に対する variant の追加が破壊的変更になりうる
      • 新しい variant が来ることを考慮してなくて、突然アプリケーションが壊れる可能性がある
      • 自分たちで実装している GraphQL API なら、variant の追加タイミングを調整できるので問題にならないようにもできる
      • しかし、外部の GraphQL API サーバーを利用している場合は、追加タイミングを予知できないので、ある日突然壊れる
    • そのため、Relay は未知の値が来ても壊れないよう実装することを推奨してる
      • それを開発者に意識させるために、relay-compiler が生成する Enum の型には'%future added value'という文字列が追加されてる
        • // from: https://github.com/facebook/relay/issues/2351#issue-300717245
          // ./src/events/__generated__/my_generated.graphql.js
          ...
          export type EVENT_TYPES = ('activity' | 'formation' | '%future added value');
          export type Event = {
            type: EVENT_TYPES
          }
          
      • このおかげで、未知の値を意識しながらコードを書くことになる
        • export const stringifyEvent = (eventType: EVENT_TYPES) => {
            if (eventType === 'activity') return 'アクティビティ'
            if (eventType === 'formation') return '編成'
            return ''
          
      • あるいは以下のように網羅性チェックをしても良い
        • この書き方だと、突然未知の値が来ても表示崩れが起きなくて、かつ tsc でコンパイルエラーにもできる
        • export const stringifyEvent = (eventType: EVENT_TYPES) => {
            if (eventType === 'activity') return 'アクティビティ'
            if (eventType === 'formation') return '編成'
            const _exhaustiveCheck: '%future added value' = eventType
            return ''
          }
          
    • とはいえいちいち未知の値のハンドリングをするのは面倒という議論もある
      • そもそも、GraphQL API を自分たちで作っているなら、variant の追加タイミングも制御可能なので、未知の値のハンドリングは適当でも良いはず
      • そうしたユースケースのために noFutureProofEnums オプションがある
      • noFutureProofEnums: trueを使うなら、以下のように書ける
        • export type EVENT_TYPES = ('activity' | 'formation');
          export type Event = {
            type: EVENT_TYPES
          }
          
        • export const stringifyEvent = (eventType: EVENT_TYPES) => {
            if (eventType === 'activity') return 'アクティビティ'
            if (eventType === 'formation') return '編成'
            return unreachable(eventType)
          }
          
      • 将来EVENT_TYPESが取りうる variant が増えたら、コンパイルエラーになる
        • export type EVENT_TYPES = ('activity' | 'formation' | 'notification');
          export type Event = {
            type: EVENT_TYPES
          }
          
        • export const stringifyEvent = (eventType: EVENT_TYPES) => {
            if (eventType === 'activity') return 'アクティビティ'
            if (eventType === 'formation') return '編成'
            return unreachable(eventType)
            //                 ^^^^^^^^^
            //                 Argument of type 'string' is not assignable to parameter of type 'never'.
          }
          

'%future added value' の仲間

Relay には '%future added value' 以外にも、'%other' という未知の値を表す文字列がある。前者が Enum の variant 向けであるのに対し、後者は interface の __typename 向けの文字列となってる。どちらも未知の値が来ても壊れないよう実装することを強制するために存在する文字列で、noFutureProofEnums オプションで生成を抑制できる。

君たちはどう '%future added value', '%other' と向き合うのか

以上を踏まえて、どう'%future added value', '%other' と向き合うべきか。

  • 3つ書き方を紹介したが、どれでも良い気はする
  • 改めて 3 つについて要点を抑えながら取り上げ直すと...
  • 未知の値だったら シンプルにfallback するのが、最もオーソドックス
    • export type EVENT_TYPES = ('activity' | 'formation' | '%future added value');
      export const stringifyEvent = (eventType: EVENT_TYPES) => {
        if (eventType === 'activity') return 'アクティビティ'
        if (eventType === 'formation') return '編成'
        return ''
      }
      
  • 素朴に網羅性チェックをしたいなら以下
    • export const stringifyEvent = (eventType: EVENT_TYPES) => {
        if (eventType === 'activity') return 'アクティビティ'
        if (eventType === 'formation') return '編成'
        const _exhaustiveCheck: '%future added value' = eventType
        return ''
      }
      
    • variant が追加されたらコンパイルエラーにして、コードを変更するべきところを検出できるのは便利だが... variant を追加するのと一緒に、コンパイルエラーが出たところを修正していかないといけないのはちょっと大変
      • 同時にやるべき作業量が多い
      • まあでもこれはそういうものかも。歯を食いしばって修正していったらよい。
      • 面倒だったら// @ts-expect-error TODO: あとで直すとか書いておけば良い
  • GraphQL API を自分たちで作っていて、noFutureProofEnums オプションをオンにする覚悟があるなら、以下がオススメ
    • export type EVENT_TYPES = ('activity' | 'formation');
      export type Event = {
        type: EVENT_TYPES
      }
      
    • export const stringifyEvent = (eventType: EVENT_TYPES) => {
        if (eventType === 'activity') return 'アクティビティ'
        if (eventType === 'formation') return '編成'
        return unreachable(eventType)
      }
      
    • 未知の variant のハンドリングをしなくて良いのでシンプル
    • けど variant を追加した瞬間にアプリケーションが壊れる可能性があるので注意
      • ダウンタイムを可能な限り小さくするには、事前にフロントエンド側を改修して、新規の variant に対応したコードに更新しておくとか、PR を用意しておいて、variant が追加された直後にデプロイできるようにしておくとか、そういう運用が必要
      • バックエンドとフロントエンドをモノレポで運用しているなら、バックエンドで variant を追加する時に、フロントエンドも一緒に直してしまう
    • とはいえ多少のダウンタイムを許容できるならアリ
      • GraphQL API が variant を追加してから、フロントエンド側の改修が完了するまでの間、開発環境が壊れるのは許容する、とか
      • 本番環境にデプロイするまでには直ってるから OK、みたいな
    • そもそもスキーマ上 variant が追加されても、実際にその variant のデータが GraphQL API から返ってくるまでには、通常猶予がある
      • スキーマだけ変更しても、まだその variant を返す実装が入ってないとか
      • いきなりページが見れなくなるほどぶっ壊れることはそうないはず
  • あとこれはオプションだけど、素朴に if 使って書くのが面倒なら、ts-pattern 導入するとかが良いと思う

感想

3つ目の noFutureProofEnums オプションを ON にしつつ、網羅性チェックするのかかなり格好良いが、本当に noFutureProofEnums オプションを ON にして運用上問題が起きないのか、正直自信がない。

一般的な Relay 仕草から外れるからには、それ相応の自信を持ってやらないといけないが、その自信を形成するだけの判断材料が僕にはない。直感では大丈夫だと思うけど...実際どうかな...。「noFutureProofEnums オプション ON にしてやってるけど、全然困ってないぜ!」という人が居たら教えてください。

自分たちの管理下にない GraphQL API を参照しているアプリケーションを作っているのなら、1つ目か2つ目の書き方のどちらかを選択すると良いと思う。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.