前書き
ちょっとしたストーリーです。
不要な方は Conditional Typesを適用してみよう まで飛んでね。
ある日のでき事です。
「このコンポーネント、フッターなしで使いたいんですが、フラグで分岐していいですか?」
そんなissueが飛んできました。
私もちょうど同じ様な使い方がしたかったので、「是非お願いしますー」と返事をしました。
before/after
before
const Table: React.FC<{
count: number;
}> = (props) => (
<div>
{props.children}
<Footer count={props.count} />
</div>
);
after
const Table: React.FC<{
count?: number;
withFooter?: boolean;
}> = (props) => (
<div>
{props.children}
{props.withFooter !== false
? <Footer count={props.count} />
: null
}
</div>
);
既存の利用箇所を考慮して、「デフォルトではフッターありで、withFooter===falseを指定するとなしにできる」と拡張されました。
ちょっとした問題
やってもらえてラッキー、だったんですがちょっとした問題ができました。
withFooter=trueの時はcountは必須で欲しいのですが、
withFooter=falseの時は不要なので、両方を満たすためにwithFooter?:と任意項目になっています。
これではwithFooter=trueの時にcountを渡し忘れてもrenderしてみるまで気がつけないのです。
型安全にしてみよう
問題点の整理
- プロパティの
countは、同じくプロパティのwithFooterがtrueなら必須、falseなら不要なのに、両方を満たすために任意とされている - 上記から、型としては
true+countなしまたはfalse+countありが許されるが、前者は動かず、後者は無駄 -
Footerコンポーネントを分離すれば良いという考えもあるが、書ききれない背景を考慮するとコストが見合わなかった
Conditional Types
そこで使いたいのが Conditional Typesです。
これは簡単に言うと、条件によって分岐する型です。(そのまま)
三項演算子に似た形で書き、extendsの左が右に置き換えられるならtrue相当の動きをします。
下記の場合はDogはAnimalに置き換え可能でFoo: numberとなります。
interface Dog extends Animal
type Foo = Dog extends Animal ? number : string;
Conditional Typesを適用してみよう
1.props型を作る
事前準備で、分岐用専用の型としてWithFooter,WithoutFooterを作りました。
type WithFooter = 'withFooter';
type WithoutFooter = 'withoutFooter';
そしてこちらがメインです。
T型にWithoutFooterを指定するとWithoutFooterPropsになり、それ以外(指定なしを含む)ではWithFooterPropsになります。
withFooterがbooleanではなく、?: true false となっているのもポイントです。
type WithFooterProps = React.PropsWithChildren<{
withFooter?: true,
count: number;
}>;
type WithoutFooterProps = React.PropsWithChildren<{
withFooter: false,
}>;
type TableProps<T = WithFooter> = T extends WithoutFooter
? WithoutFooterProps
: WithFooterProps;
2.コンポーネントに適用
genericsが必要になりますが、React.FCでは上手く書けなかったので分解しています。
また、<T, >とは、「これはjsxではなくてgenericsだよ」、とパーサーにわかってもらうための小技です。
const Table = <T, >(props: TableProps<T>): React.ReactNode => (
Type Guardも作って適用しましょう。
const isWithFooter = (props: WithFooterProps|WithoutFooterProps): props is TableProps<WithFooter> => props.withFooter !== false;
{isWithFooter(props)
? <Footer count={props.count} />
: null
}
3.利用箇所に適用
// フッターありの場合
<Table count={count} />
// フッターなしの場合
<Table<WithoutFooter> withFooter={false} />
まとめ
無事に型安全にpropsの分岐を作ることができました。
もちろんコンポーネント自体をリファクタリングした方が良いというのもありますが、
選択肢の一つとして持っておけると、いざという時に役に立つかと思います。