環境情報
- 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
を保持しています。
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
属性を指定しないとコンパイルエラーとなります。
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です。
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
属性を指定しない場合はデフォルト値が適用されるため、コンパイルエラーになることはありません。
class App extends React.Component {
public render() {
// OK
return <Button />;
}
}
一部のプロパティのみデフォルト値を定義
先ほどは全プロパティのデフォルト値を定義していましたが、一部のプロパティのみデフォルト値を定義したい場合はどうすればいいでしょうか。例えばonClick
プロパティはデフォルト値を定義せず、外部から必ず指定させたいというケースです。
defaultProps
の型にProps
を指定しているため、一部の項目のみを定義することはできません。
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型を指定してあげれば、解決すると思ってました。
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
属性を指定しなかった場合にコンパイルエラーとならず、予期せぬ実行時エラーを招く結果となります。
class App extends React.Component {
public render() {
// コンパイルエラーとならず、onClickにはundefinedが設定されてしまう
return <Button />;
}
}
対応案2: defaultPropsの型を明示的に定義せず型推論させる
defaultPropsの型として明示的にProps
を指定していましたが、これをやめ型推論を利用してみます。
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
属性を指定しない場合は、コンパイルエラーとなることが確認できます。
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の恩恵に与ることができません。うっかりタイポしてしまった場合でもコンパイルエラーで気づくことができないのです。
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です。
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
属性を指定しない場合は、コンパイルエラーとなることが確認できました。
// 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 />;
}
}
ただし、このままではProps
とDefaultProps
の定義が重複しておりメンテナンス性が悪いため、Props
はDefaultProps
を継承するようにします。
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",
};
...
}
これでばっちりです。