27
9

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 1 year has passed since last update.

React #2Advent Calendar 2019

Day 14

@types/react の中を少し読んでみる

Last updated at Posted at 2019-12-13

この記事は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.tsindex.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

言わずとしれたクラスコンポーネントの型です。
正確にはクラスなのですが重要なのでとりあげます。

型定義
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }

実際はクラスなので継承して使うことになります。

type Props = {};
type State = {};
class Sample extends React.Component<Props, State> {
}

これでthis.propsthis.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.Componentrender()の戻り値など色んな所で登場してくる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

こちらは自作のコンポーネントではなくdivinputなどの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など関数的な考え方を持ち込んでいるライブラリは型定義が面白かったりと勉強になるので
皆さんもぜひ読んでみてはいかがでしょうか。


おしまい

27
9
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
27
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?