6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React + TypeScript でフラグ分岐するpropsを型安全に作る

Last updated at Posted at 2020-12-27

前書き

ちょっとしたストーリーです。
不要な方は 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は、同じくプロパティのwithFootertrueなら必須、falseなら不要なのに、両方を満たすために任意とされている
  • 上記から、型としてはtrue+countなしまたはfalse+countありが許されるが、前者は動かず、後者は無駄
  • Footerコンポーネントを分離すれば良いという考えもあるが、書ききれない背景を考慮するとコストが見合わなかった

Conditional Types

そこで使いたいのが Conditional Typesです。
これは簡単に言うと、条件によって分岐する型です。(そのまま)
三項演算子に似た形で書き、extendsの左が右に置き換えられるならtrue相当の動きをします。
下記の場合はDogAnimalに置き換え可能で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になります。
withFooterbooleanではなく、?: 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の分岐を作ることができました。
もちろんコンポーネント自体をリファクタリングした方が良いというのもありますが、
選択肢の一つとして持っておけると、いざという時に役に立つかと思います。

参考リンク

6
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?