この記事は2019/12時点の記事です。
Reactのバージョンも上がりReact.VFC
なども登場しているのでご注意ください
はじめに
ここ最近はTypeScriptを利用することがデファクトスタンダードになりつつありますね。
そこでその際に必要になるReactの型定義(@types/react
)について色々おさらいしてみます。
対象のパッケージはこちら
npm: https://www.npmjs.com/package/@types/react
GitHub: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react
解説
@types/react
ではglobal.d.ts
とindex.d.ts
の2つのファイルが提供されていますので
それぞれを見ていきましょう。
global.d.ts
見てみるとわかるのですが空のinterface定義が並んでいます。
そしてファイルの先頭に下記のコメントが。
/*
React projects that don't include the DOM library need these interfaces to compile.
React Native applications use React, but there is no DOM available. The JavaScript runtime
is ES6/ES2015 only. These definitions allow such projects to compile with only `--lib ES6`.
Warning: all of these interfaces are empty. If you want type definitions for various properties
(such as HTMLInputElement.prototype.value), you need to add `--lib DOM` (via command line or tsconfig.json).
*/
適当に訳すと
「これらの定義を用意することでlib.dom
を読み込まなくてもコンパイルできるようになります」
「ただし、空の定義なのでプロパティなどにアクセスしたいのならちゃんと読み込んでね」
ってことみたいです。
大体はlib.dom
を読み込むと思うのでこのファイルはあまり気にしなくていいかもしれません。
index.d.ts
それではReact本体の型定義を見ていきます。
今回紹介する定義は以下になります。
- React.Component
- React.ComponentClass
- React.FunctionComponent
- React.ComponentType
- React.ComponentProps
- React.ReactNode
- React.ComponentProps
- React.ReactDOM
React.Component
言わずとしれたクラスコンポーネントの型です。
正確にはクラスなのですが重要なのでとりあげます。
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
実際はクラスなので継承して使うことになります。
type Props = {};
type State = {};
class Sample extends React.Component<Props, State> {
}
これでthis.props
やthis.state
,this.setState
などが型安全になりました。
contextType
クラスの定義をよく読むとこんな記述が。
static contextType?: Context<any>;
context: any;
これはContextAPIをクラスコンポーネントで利用する際の型定義になるのですが
any
と書かれているので実際に使う際は以下のようにしてあげましょう。
const hogeCtx = React.createContext({ count: 0 });
class Child extends React.Component<Props, State> {
static contextType = hogeCtx;
context!: React.ContextType<typeof hogeCtx>;
render() {
return <h1>{this.context.count}</h1>;
}
}
TypeScript3.7以降であれば以下の書き方になります
class Child extends React.Component<Props, State> {
static contextType = hogeCtx;
declare context: React.ContextType<typeof hogeCtx>
// ↑ここが変数の再定義ではなく型の定義のみでよい
...
こうすることで this.context
が型安全に利用することができます。
注記
React 16.3より前から存在するLegacy Context
についても同様にthis.context
を利用していました。
その場合はcontextType
の定義はせずany
のまま利用するほうが良いかと思います。
React.ComponentClass
これはReact.Component
などのインターフェースになります。
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
new (props: P, context?: any): Component<P, S>;
propTypes?: WeakValidationMap<P>;
contextType?: Context<any>;
contextTypes?: ValidationMap<any>;
childContextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
クラスとインターフェースと何が違うのかというと言語化が難しいのですが以下のような挙動になります。
type Props = {};
class Hoge extends React.Component<Props> {
}
const hoge: Hoge = Hoge; // これはエラー
const hogeType: React.ComponentClass<Props> = Hoge;
クラスにするということはインスタンスの型となるため、
純粋なコンポーネントのインターフェースが欲しい場合はこちらの型を使うことになります。
具体的な利用例は後述するReact.ComponentType
でご紹介します。
React.FunctionComponent
関数型コンポーネントの型となるFunctionComponent
です。
最近はクラスコンポーネントよりもこちらのほうが主流ですね。
エイリアスとしてReact.FC
というものもあります。(私は短いほうが好きなのでこちらを多用しています
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
type FC<P = {}> = FunctionComponent<P>;
ここではそれぞれのプロパティについて説明します。
関数本体
関数の第2引数はLegacy Context
の値になるので最近だと利用することはないです。
(非推奨なのでanyのまま放置されているのだと思います)
propsTypes
propsTypes
もありますがTypeScriptの場合はコンパイルエラーでカバーできるので
わざわざ記述することはなくなりました。
contextTypes
contextTypes
についてもLegacy Context
関連なので無視!
defaultProps
こちらはprops
に対してデフォルト値を与えるものになります(そのまま
個人的にはdefaultProps
で定義した項目はoptionalじゃなくできるようになってほしいです...
displayName
これを設定しておくことでデバッグ時などにコンポーネント名として表示してくれます。
minify時に関数名などは消えてしまいデバッグが辛いので極力設定しましょう!
ちなみに
Hooksが出てくるまではStatelessFunctionComponent
(SFC
)というものがありましたが
現在は非推奨になっているためこちらに切り替えましょう。
React.ComponentType
まずは型定義
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
上記で説明した「クラスコンポーネントor関数コンポーネント」という型で、
「とにかくコンポーネントがほしい!!」って場合に利用します。
具体的な例はこちら
interface WithHoge {
hoge: string
}
function withHoge<P extends WithHoge>(
Component: React.ComponentType<P>
): React.FC<Omit<P, keyof WithHoge>> {
return (props) => {
const inProps = {...props, hoge: 'value' } as P;
return <Component {...inProps} />;
}
}
このようなHOCを作ったりする場合は、
関数コンポーネントでもクラスコンポーネントでもよいのでComponentTypeを利用すると良いです。
その他にもコンポーネントそのものを受け渡す場合はこちら利用していきましょう。
React.ReactNode
React.Component
のrender()
の戻り値など色んな所で登場してくるReact.ReactNode
。
簡単に言うと「JSXの中で存在できる要素」を指します。(この表現が正しいかはあやしい)
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
// 関連↓
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
コンポーネントでも文字列でも数字でもなんでも来い!っていう定義で
「とにかくJSXの中に突っ込みたいけど型がバラバラ」といった場合に活躍します。
ちなみに
関数コンポーネント(FunctionComponent
)の戻り値はReactElement
です。
なのでundefined
や数値など適当なものは返却できないようになっています。
(Component.renderはできるのに)
React.ComponentProps
これは便利な型として紹介いたします。
コンポーネントからPropsのを抜き出す際に利用します。
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
T extends JSXElementConstructor<infer P>
? P
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: {};
infer
で目的の型を抜き出しているのですが難しい定義ですね。
使用例)
const Sample = (props: { name: string; age: number }) => {
return <>hello</>;
};
type SampleProps = React.ComponentProps<typeof Sample>;
/*
type SampleProps = {
name: string;
age: number;
}
*/
このようにコンポーネントからPropsの型を抜き出すことができました。
利用しているライブラリでProps
が公開されていない場合でも型を抜き出すことができるので重宝します。
React.ReactDOM
こちらは自作のコンポーネントではなくdiv
やinput
などのHTML,SVGタグの定義となります。
interface ReactDOM extends ReactHTML, ReactSVG { }
interface ReactHTML {
a: DetailedHTMLFactory<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
abbr: DetailedHTMLFactory<HTMLAttributes<HTMLElement>, HTMLElement>;
address: DetailedHTMLFactory<HTMLAttributes<HTMLElement>, HTMLElement>;
....
}
interface ReactSVG {
animate: SVGFactory;
circle: SVGFactory;
...
}
これを単体で使うことはあまりないのですがComponentProps
と組み合わせると以下のようなことが可能です。
type Div = React.ReactDOM['div'];
type DivProps = React.ComponentProps<Div>;
const props: DivProps = { className: 'class-name' };
return <div {...props} />
自作コンポーネントじゃないタグに動的なprops
を設定したいとき、
型安全な変数として定義することができます。
このようなショートハンドの型を定義するともっと使いやすくなりそうです。
type DOMProps<E extends keyof React.ReactDOM> = React.ComponentProps<React.ReactDOM[E]>;
// 使う
type DivProps = DOMProps<'div'>;
追記
JSX.IntrinsicElements
という型が存在しており
これを使うことで上記と同じようなことができます。
そしてどうやらIntrinsicElements
のほうがスタンダードのようです...!
まとめ
実際にはコンポーネントの型定義が他にもいくつかあって複雑に絡み合っているのですが
今回はこのあたりのよく目にする部分のみをまとめてみました。(時間がなかった)
Reactなど関数的な考え方を持ち込んでいるライブラリは型定義が面白かったりと勉強になるので
皆さんもぜひ読んでみてはいかがでしょうか。
おしまい