LoginSignup
3
3

はじめに

近年では、JavaScriptに型の構文を持たせる手法としてTypeScriptが広く採用されています。
しかし、TypeScript以外にもJavaScriptに型の構文を持たせるツールは存在します。そのひとつがMetaによって開発されているFlowです。

FlowはMeta社によって開発されていることもあり、TypeScriptにはないReactに特化した型を持ちます。

この記事ではFlowの構文を簡単に紹介した後に、Reactに特化したFlowだけの構文を紹介します。

Flowに興味を持つきっかけや、React開発に対するなんらかのインスピレーションになればと思います。

Flow

Flowが提供する型の構文はTypeScriptと似ています。全ては書ききれませんが、基本的な型について紹介します。

変数に対する型は: で付与します。

const num: number = 12;
const isCorrect: true = true;

TypeScriptと同じようにプリミティブ型とリテラル型が提供されています。TypeScriptと異なる点としては、undefinedの型がvoidで表現される点です。

独特な型としてはmixedemptyany、Maybe型があります。

mixedはすべての型のスーバータイプです。TypeScriptにおけるunknown型と似ています。
あらゆる型の値を受け入れますが、その値を操作するときはすべての型に対して可能な操作しかできません。

emptyはすべての型のサブタイプです。mixedの逆です。

anyはあらゆる型として扱える型です。TypeScriptのanyと同じく安全に扱えません。

Maybe型は型の前に?を付けることで、付けた型とnullvoidの共用型を表現できます。

// mixedを利用する例: 任意の値を受け取ってその値のオペランドの型を返す関数
const getType = (value: mixed): string => typeof value;

const getVariant = (
  // |で共用型を表現します
  value: 'success' | 'warn' | 'error'
): string => {
  switch (value) {
    case 'success':
      return '#259D63';
    case 'warn':
      return '#B78F00';
    case 'error':
      return '#EC0000';
    default:
      // emptyを利用する例: switchでvalueを網羅していることを明示的に示す
      return (value: empty);
  }
};

// Maybe型を利用する例: valueにnumberの他にnullとundefinedを受け取り可能にする
const getNumber(value: ?number): number => {
  if (typeof value === 'number') {
    return value;
  }
  return 0;
}

例からは関数の構文がTypeScriptと大きく変わらないことも見て取れます。オブジェクト型も同じく、TypeScriptのように定義できます。

type Obj1 = {
  first: number,
  // オプションのプロパティ
  second?: 2,
  // 読み取り専用のプロパティ
  +third: ?number,
};

// stringをキーに、numberを値に持つオブジェクト
interface Obj2 { [string]: number };

型自体はtypeinterfaceを用いて定義されます。

この他にもTypeScriptがユーティリティ型として提供する、指定した型を読み取り専用にするような型や、コメントを利用して型を付与するようなFlow特有の記法があります。
詳細が気になる場合は公式ドキュメントを確認してください。
TypeScriptにない型やTypeScriptとは異なる型表現があり、とても興味深いです。

Reactに特化した型表現

FlowはReactと一緒に使われることが多いので、React専用の構文を提供しています。

コンポーネントを宣言する構文、React Hooksを宣言する構文、型レベルで特定のコンポーネントを表現する構文があります。

これらの構文は快適で堅牢なReact開発のために、記述するコードの削減と統一、型安全性の向上、Reactのルールの遵守をします。

コンポーネントを宣言する構文

TypeScriptと同じようにコンポーネントを宣言する場合、JavaScriptのコードに対して型を付与する形で以下のように記述されます。

// $ReadOnlyはオブジェクトを読み取り専用にするFlowのユーティリティ型
type Props = $ReadOnly<{
  text: string,
  onClick: () => void,
}>;

export function Button({
  text,
  onClick,
}: Props): React.MixedElement {
  return <button onClick={onClick}>{text}</button>;
}

FlowではこのコードをReactのために用意された構文を用いて以下のようにも書けます。

export component Button(
  text: string,
  handleClick: () => void
) {
  return <button onClick={handleClick}>{text}</button>;
}

Reactコンポーネントをfunctionではなくcomponentを用いて宣言しました。

componentを用いた構文では、元の宣言に対して以下の変化があります。

  • propsをオブジェクトではなく個別のパラメータとして受け取るように
  • 戻り値の型がReact.Nodeのサブタイプであるように
  • 全ての分岐で明示的に戻り値を持つように
  • レンダリング中のrefの読み書きを禁止に

propsをオブジェクトではなく個別のパラメータでとしてけ取るように

オブジェクトで受け取る場合はtexthandleClickという文字列を値と型の両方で記述する必要がありましたが、個別のパラーメータによって一度の記述で済むようになります。

// before
function Button({
  title,
  handleClick,
}: {
  title: string,
  handleClick: () => void
})

// after(記述量が軽減)
component Button(title: string, handleClick: () => void)

さらに、propsは自動的に読み取り専用になります。

export component Button(
  text: string,
  handleClick: () => void
) {
  // エラーが生じる
  text = '検索する';
  return <button onClick={handleClick}>{text}</h1>;
}

表面的に読み取り専用になるのではなく、深いところまで面倒を見てくれます。

export component Sample(
  user: {
    id: number,
    friendIds: Array<number>,
  }
) {
  // エラーが生じる
  user.id = '検索する';
  
  // エラーが生じる
  user.friendIds = [];
  
  const friendIds = user.friendIds;
  // エラーが生じる
  friendIds.[0] = 4;
  return ...;
}

component構文を用いない宣言ではReadonlyを付与する対象が浅くなったり、付与し忘れたり、そもそも付与するようなルールがなかったりします。

少ないコード量でReactのルールを逸脱したpropsの利用を防ぎつつ、記述のブレを少なくしてくれる良い制約に感じます。

戻り値の型がReact.Nodeのサブタイプであるように

戻り値は自動的にReact.Nodeのサブタイプとなるような制約が課されます。

component Sample() {
  // エラーが生じる
  return new Object();
}

component構文を用いない宣言では、まれに戻り値の型をつけ忘れてコンポーネントとして扱えない戻り値が含まれるあるので、制約が自動的に課されるのは嬉しいです。

戻り値は後に紹介する「型レベルで特定のコンポーネントを表現する構文」を用いて明示的に付与できます。

全ての分岐で明示的に戻り値を持つように

暗黙的にundefined等の戻り値を返すようになっている場合であっても、各分岐で明示的な値を返す必要があります。

component Sample(loading: boolean) {
  if (loading) {
    return <h1>loading...</h1>;
  }
  // エラーが生じる
}

typescript-eslintexplicit-function-return-typeのような制約で、潜在的なバグを防いでくれます。

レンダリング中のrefの読み書きを禁止に

Reactの公式ドキュメントにあるようにレンダリング中にrefの値の読み書きは推奨されていません。component構文ではレンダリング時点に計算される箇所でrefの読み書きをするとエラーが生じるようになっています。

component Sample() {
  const ref = useRef<number>(0);
  // エラーが生じる
  ref.current = 1;

  // エラーが生じる
  return <div>{ref.current}</div>
}

refの誤用は気付きにくく、いつの間にかコードに埋まっていることも多いので、自動的に検知してくれるのは助かります。

React Hooksを宣言する構文

Reactコンポーネントをcomponentで宣言したように、React Hooksはhookを用いて宣言します。

hook useOpen(initial: boolean): {
  isOpen: boolean,
  onOpen: () => void,
  onClose: () => void,
} {
  const [isOpen, setIsOpen] = useState(initial);

  const onOpen = useCallback(() => {
    setIsOpen(true);
  }, []);
  
  const onClose = useCallback(() => {
    setIsOpen(false);
  }, []);

  return {
    isOpen,
    onOpen,
    onClose,
  };
}

hook構文は関数名に対して、Reactが設ける命名規則通りの名前であるこを課します。例えば上記のケースでuse-online-stateonlineStatusのような名前にするとエラーが発生します。

この他にも、component構文と同じような制限や、ReactのESLintで課すようなReact Hooksのルールを検査してくれます。

hook useSample1(arg: string) {
  // エラーが生じる
  const arg = 'hello';

  const ref = useRef<number>(0);
  // エラーが生じる
  ref.current = 1;
}

hook useSample2() {
  if (isClientComponent) {
    // 分岐内でstateを用いているのでエラーが生じる
    const [state, setState] = useState();
  }
}

型レベルで特定のコンポーネントを表現する構文

どのようなコンポーネントを返すか、どのようなコンポーネントを受け取るかをrendersによって指定できます。

component Header(size: 'sm' | 'lg') {
  return (
    <header>
      ヘッダーだよ
    </header>
  );
}

// Headerコンポーネントを返す
component LargeHeader() renders Header {
  return <Header size="lg" />;
}

// Headerコンポーネントを受け取る
component Layout(header: renders Header) {
  return (
    <div>
      {header}
      <main>メインのコンテンツだよ</main>
    </div>
  );
}

<Layout header={<Header />} />
<Layout header={<LargeHeader />} />
// エラーが生じる
<Layout header={<div />} />

LayoutコンポーネントはHeaderコンポーネントを引数に受け取ります。HeaderコンポーネントとHeaderコンポーネントを返すLargeHeaderコンポーネント以外のコンポーネントを渡すとエラーが発生します。

複数の同一のコンポーネントを受け取る場合はrenders*を利用します。

component Item() {
  return <li>item</li>;
}

component List(
  children: renders* Item,
) renders Header {
  return <ul>{children}</ul>;
}

<List>
  <Item />
</List>

<List>
  <Item />
  <Item />
</List>

構成するコンポーネントに対する制限を課され実装の選択肢が少なくなることで、実装の見通しが立ちやすく、一貫性を持った開発を行えます。

おわりに

この記事では、Flowの基本的な型の構文と、Reactに特化したFlowの独自構文について紹介しました。FlowはMeta社によって開発されており、Reactのための型や構造が提供されています。

componenthookを用いた宣言方法は、Reactのルールを遵守しながら開発者の負荷を軽減します。
また、rendersによる型レベルでのコンポーネント制御は、UI一貫性を保たせつつ、高い開発体験を実現します。

この記事がFlowに興味を持つきっかけや、React開発に対する新たなインスピレーションとなれば幸いです。
是非、ReactプロジェクトにFlowを取り入れてみたり、Flowの構文を元にした制限をTypeScriptを使ったプロジェクトで模してみてはいかがでしょうか。

3
3
0

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
3
3