この記事は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など関数的な考え方を持ち込んでいるライブラリは型定義が面白かったりと勉強になるので
皆さんもぜひ読んでみてはいかがでしょうか。
おしまい