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

## 環境情報

* TypeScript: 3.2.2
* React: 16.7.0

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

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

```ts
class SomeComponent extends React.Component<SomeProps, SomeState> {
    public static defaultProps: SomeProps = {
        prop1: "default"
    };
    ...
}
```

## サンプルコンポーネント

今回説明を行うに当たって下記のような`Button`コンポーネントを考えてみましょう。
`Button`コンポーネントはプロパティとして`onClick`, `color`, `type`を保持しています。

```tsx: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`属性を指定しないとコンパイルエラーとなります。

```tsx: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です。

```tsx: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`属性を指定しない場合はデフォルト値が適用されるため、コンパイルエラーになることはありません。

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

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

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

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

```tsx: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型を指定してあげれば、解決すると思ってました。

```tsx: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`属性を指定しなかった場合にコンパイルエラーとならず、予期せぬ実行時エラーを招く結果となります。

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

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

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

```tsx: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`属性を指定しない場合は、コンパイルエラーとなることが確認できます。

```tsx: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の恩恵に与ることができません。うっかりタイポしてしまった場合でもコンパイルエラーで気づくことができないのです。

```tsx: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です。

```tsx: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`属性を指定しない場合は、コンパイルエラーとなることが確認できました。

```tsx: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 />;
  }
}
```

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

```tsx: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",
  };
  ...
}
```

これでばっちりです。

## 参考

* https://medium.com/@martin_hotell/react-typescript-and-defaultprops-dilemma-ca7f81c661c7
* https://qiita.com/r-tamura/items/b09683889cd7eb5e7c43