Edited at

【React + TypeScript】コンポーネントのdefaultPropsの型をきちんと指定する


環境情報


  • TypeScript: 3.2.2

  • React: 16.7.0


前提知識: デフォルトプロパティの定義

Class Componentにおけるデフォルトプロパティを定義する際は、下記の通りdefaultPropsフィールドを用意してあげます。

class SomeComponent extends React.Component<SomeProps, SomeState> {

public static defaultProps: SomeProps = {
prop1: "default"
};
...
}


サンプルコンポーネント

今回説明を行うに当たって下記のようなButtonコンポーネントを考えてみましょう。

ButtonコンポーネントはプロパティとしてonClick, color, typeを保持しています。


Button.tsx

interface Props {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
color: "blue" | "green" | "red";
type: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Render DOM.
*/

public render() {
return <button type={this.props.type} style={{ color: this.props.color }} onClick={this.props.onClick} />;
}
}


このコンポーネントはデフォルトプロパティを定義していないため、Buttonコンポーネントを利用する側がonClick, color, type属性を指定しないとコンパイルエラーとなります。


App.tsx

class App extends React.Component {

public render() {
// Type error: Type '{}' is missing the following properties from type 'Readonly<InputProps>': onClick, color, type TS2739
return <Button />;
}
}


デフォルトプロパティを定義

全プロパティのデフォルト値を定義する場合は下記のようにすればOKです。


Button.tsx

interface Props {

onClick?: (e: React.MouseEvent<HTMLElement>) => void;
color?: "blue" | "green" | "red";
type?: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

public static defaultProps: Props = {
onClick: _ => console.log("clicked"),
color: "blue",
type: "button",
};
...
}


これでButtonコンポーネントを利用する側がonClick, color, type属性を指定しない場合はデフォルト値が適用されるため、コンパイルエラーになることはありません。


App.tsx

class App extends React.Component {

public render() {
// OK
return <Button />;
}
}


一部のプロパティのみデフォルト値を定義

先ほどは全プロパティのデフォルト値を定義していましたが、一部のプロパティのみデフォルト値を定義したい場合はどうすればいいでしょうか。例えばonClickプロパティはデフォルト値を定義せず、外部から必ず指定させたいというケースです。

defaultPropsの型にPropsを指定しているため、一部の項目のみを定義することはできません。


Button.tsx

interface Props {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
color?: "blue" | "green" | "red";
type?: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

// Type error: Property 'onClick' is missing in type '{ color: "blue"; type: "button"; }' but required in type 'Props'. TS2741
public static defaultProps: Props = {
color: "blue",
type: "button",
};
...
}



対応案1: defaultPropsの型をPartialにする

当初はdefaultPropsの型をPartial<Props>とすることで部分的なProps型を指定してあげれば、解決すると思ってました。


Button.tsx

interface Props {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
color?: "blue" | "green" | "red";
type?: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

public static defaultProps: Partial<Props> = {
color: "blue",
type: "button",
};
...
}


しかしPartialを利用すると、onClick属性の型が(e: React.MouseEvent<HTMLElement>) => void | undefinedとなってしまい、onClick属性を指定しなかった場合にコンパイルエラーとならず、予期せぬ実行時エラーを招く結果となります。


App.tsx

class App extends React.Component {

public render() {
// コンパイルエラーとならず、onClickにはundefinedが設定されてしまう
return <Button />;
}
}


対応案2: defaultPropsの型を明示的に定義せず型推論させる

defaultPropsの型として明示的にPropsを指定していましたが、これをやめ型推論を利用してみます。


Button.tsx

interface Props {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
color?: "blue" | "green" | "red";
type?: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

public static defaultProps = {
color: "blue",
type: "button",
};
...
}


onClick属性を指定しない場合は、コンパイルエラーとなることが確認できます。


App.tsx

class App extends React.Component {

public render() {
// Type error: Property 'onClick' is missing in type '{}' but required in type 'Pick<Readonly<{ children?: ReactNode; }> & Readonly<Props>, "children" | "onClick">'. TS2741
return <Button />;
}
}

しかしこの場合、defaultPropsにどんなプロパティでも定義できてしまうため、TypeScriptの恩恵に与ることができません。うっかりタイポしてしまった場合でもコンパイルエラーで気づくことができないのです。


Button.tsx

interface Props {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
color?: "blue" | "green" | "red";
type?: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

public static defaultProps = {
color: "blue",
types: "button", // タイポに気づくことができない
hoge: "hoge", // 任意のプロパティを追加できてしまう
};
...
}


ではどうすればいいでしょうか。


対応案3: defaultProps用のinterfaceを定義する

defaultProps用のinterfaceを明示的に用意してあげればOKです。


Button.tsx

interface Props {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
color?: "blue" | "green" | "red";
type?: "button" | "submit";
}

interface DefaultProps {
color: "blue" | "green" | "red";
type: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

public static defaultProps: Partial<DefaultProps> = {
color: "blue",
type: "button",
};
...
}


onClick属性を指定しない場合は、コンパイルエラーとなることが確認できました。


App.tsx

// Type error: Property 'onClick' is missing in type '{}' but required in type 'Pick<Readonly<{ children?: ReactNode; }> & Readonly<Props>, "children" | "onClick">'.  TS2741

class App extends React.Component {
public render() {
return <Button />;
}
}

ただし、このままではPropsDefaultPropsの定義が重複しておりメンテナンス性が悪いため、PropsDefaultPropsを継承するようにします。


Button.tsx

interface Props extends Partial<DefaultProps> {

onClick: (e: React.MouseEvent<HTMLElement>) => void;
}

interface DefaultProps {
color: "blue" | "green" | "red";
type: "button" | "submit";
}

class Button extends React.Component<Props, {}> {
/**
* Default properties.
*/

public static defaultProps: Partial<DefaultProps> = {
color: "blue",
type: "button",
};
...
}


これでばっちりです。


参考