🗂️
判別可能なUnion型を使用してコンポーネントのバージョン管理をする
判別可能なUnion型とは
英語で言うところのDiscriminating Unions
です。
詳細な説明は以下のTypeScript Deep Dive 日本語版に譲りたいと思います。
モチベーション
そもそものコンポーネントのバージョン管理をするモチベーションとしては以下を想定しています。
- バージョンの概念を持つドメインを表示するコンポーネントがある
- バージョンが進んだ場合も過去のバージョンはその時点のデザインで表示したい
といったものです。
バックエンドやBFFで何とかするという方法もありそう(そちらの方が正攻法ではありそう)ですが、今回はフロントエンドでコンポーネントのバージョン管理をするという方法を考えてみます。
実装例
バージョンをリテラル型で表現する
今回はnumber
で表現します。
export type QuestionnaireV1 = {
version: 1
name: string
body: string
}
export type QuestionnaireV2 = {
version: 2
name: string
age: number
body: string
remarks: string
}
export type Questionnaire = QuestionnaireV1 | QuestionnaireV2
バージョンに対応したコンポーネントを作成する
type QuestionnairePageV1Props = {
questionnaire: QuestionnaireV1
}
export const QuestionnairePageV1 = ({
questionnaire,
}: QuestionnairePageV1Props) => {
return (
<div>
<div>{questionnaire.name}</div>
<div>{questionnaire.body}</div>
</div>
)
}
type QuestionnairePageV2Props = {
questionnaire: QuestionnaireV2
}
export const QuestionnairePageV2 = ({
questionnaire,
}: QuestionnairePageV2Props) => {
return (
<div>
<div>{questionnaire.name}</div>
<div>{questionnaire.age}</div>
<div>{questionnaire.body}</div>
<div>{questionnaire.remarks}</div>
</div>
)
}
バージョンに対応したコンポーネントを返すコンポーネントを作成する
type QuestionnairePageProps = {
questionnaire: Questionnaire
}
const QuestionnairePage = ({
questionnaire,
}: QuestionnairePageProps) => {
switch (questionnaire.version) {
case 1: {
return (
// questionnaireの型はQuestionnaireV1に絞られる
<QuestionnairePageV1 questionnaire={questionnaire} />
)
}
case 2: {
return (
// questionnaireの型はQuestionnaireV2に絞られる
<QuestionnairePageV2 questionnaire={questionnaire} />
)
}
default: {
const invalidVersion: never = questionnaire.version
throw new Error("Invalid questionnaire.version")
}
}
}
おわりに
バージョンに対応したコンポーネントを作成するのところでcomponents/questionnaire/v1/index.tsx
、components/questionnaire/v2/index.tsx
のようなディレクトリ構成にして、それぞれをStorybookに登録するとバージョンごとのデザインの確認も簡単にできてよいと思います。
バージョン管理以外でも判別可能なUnion型を使用して型を絞り込むテクニックはよく見るので覚えておいて損はないかもしれません。
Discussion