はじめに
近年では、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
で表現される点です。
独特な型としてはmixed
、empty
、any
、Maybe型があります。
mixed
はすべての型のスーバータイプです。TypeScriptにおけるunknown
型と似ています。
あらゆる型の値を受け入れますが、その値を操作するときはすべての型に対して可能な操作しかできません。
empty
はすべての型のサブタイプです。mixed
の逆です。
any
はあらゆる型として扱える型です。TypeScriptのany
と同じく安全に扱えません。
Maybe型は型の前に?
を付けることで、付けた型とnull
とvoid
の共用型を表現できます。
// 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 };
型自体はtype
やinterface
を用いて定義されます。
この他にも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
をオブジェクトではなく個別のパラメータでとしてけ取るように
オブジェクトで受け取る場合はtext
やhandleClick
という文字列を値と型の両方で記述する必要がありましたが、個別のパラーメータによって一度の記述で済むようになります。
// 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-eslint
のexplicit-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-state
やonlineStatus
のような名前にするとエラーが発生します。
この他にも、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のための型や構造が提供されています。
component
やhook
を用いた宣言方法は、Reactのルールを遵守しながら開発者の負荷を軽減します。
また、renders
による型レベルでのコンポーネント制御は、UI一貫性を保たせつつ、高い開発体験を実現します。
この記事がFlowに興味を持つきっかけや、React開発に対する新たなインスピレーションとなれば幸いです。
是非、ReactプロジェクトにFlowを取り入れてみたり、Flowの構文を元にした制限をTypeScriptを使ったプロジェクトで模してみてはいかがでしょうか。