前書き
ちょっとしたストーリーです。
不要な方は 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の分岐を作ることができました。
もちろんコンポーネント自体をリファクタリングした方が良いというのもありますが、
選択肢の一つとして持っておけると、いざという時に役に立つかと思います。