前提
- まず議論の土台は 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 '' }
- それを開発者に意識させるために、relay-compiler が生成する Enum の型には
- とはいえいちいち未知の値のハンドリングをするのは面倒という議論もある
- そもそも、GraphQL API を自分たちで作っているなら、variant の追加タイミングも制御可能なので、未知の値のハンドリングは適当でも良いはず
- そうしたユースケースのために
noFutureProofEnums
オプションがある- https://github.com/facebook/relay/blob/cb8a0b073ee71b6d5b66c9ab940e73f284c1e121/packages/relay-compiler/README.md?plain=1#L81
'%future added value'
を追加しないようにできる
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'. }
- GraphQL では Enum に対する variant の追加が破壊的変更になりうる
'%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つ目の書き方のどちらかを選択すると良いと思う。