🫥

ReactコンポーネントPropsの型定義はどうするのが正解?

に公開

修正

2025年8月28日 - 修正

HTMLAttributes〇〇HTMLAttributesでは、
children型は入っていない旨の内容を書いてしまいましたが、

そもそも両方ともchildren型を持つDOMAttributes型を継承しており、
children型は今回紹介する例では全て使用できます。

interface DOMAttributes<T> {
    children?: ReactNode | undefined;
    ~~~
}
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {}
ButtonHTMLAttributes<T> extends HTMLAttributes<T>{}

その他でも、全体的に描き直しています。
検証が漏れており、申し訳ありませんでした🙇

はじめに

ReactでコンポーネントPropsの型定義には、以下の5つのどれかを使うことが多いです。

  • HTMLAttributes
  • 〇〇HTMLAttributes (要素限定)
  • ComponentProps
  • ComponentPropsWithRef
  • ComponentPropsWithoutRef

例えば、これらを使わないと
以下のようにお馴染みのpropを毎回定義しないといけなくなります。

button.tsx
type Props = {
  text: string;
  className?: string; // お馴染みのやつ1
  disabled?: boolean; // お馴染みのやつ2
  onClick: ()=> void; // お馴染みのやつ3
}

const Button:React.FC<Props> = (props)=>{
  // 以下省略 ~~~~~
}

ですが、人によって書き方がまちまちだったので、どれを採用するのがいいか調べてみました。

ソースをみよう

こちらにreactの型ファイルのソースがあるのでそれを紐解いていくことにします。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts

(1).HTMLAttributesvsButtonHTMLAttributes

両者を見比べると、ButtonHTMLAttributesHTMLAttributesを継承しています。
disabledtypeなどButtonHTMLAttributesにしかないため、該当するコンポーネントの〇〇HTMLAttributesが用意されている場合はなるべくこちらを使った方が良さそうです。

interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
    // React-specific Attributes
    defaultChecked?: boolean | undefined;
    defaultValue?: string | number | readonly string[] | undefined;
    suppressContentEditableWarning?: boolean | undefined;
    suppressHydrationWarning?: boolean | undefined;
   // ~~~~ 以下省略
interface ButtonHTMLAttributes<T> extends HTMLAttributes<T> {
    disabled?: boolean | undefined;
    form?: string | undefined;
    formAction?:
        | string
        | ((formData: FormData) => void | Promise<void>)
        | DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS[
            keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS
        ]
        | undefined;
    formEncType?: string | undefined;
    formMethod?: string | undefined;
    formNoValidate?: boolean | undefined;
    formTarget?: string | undefined;
    name?: string | undefined;
    type?: "submit" | "reset" | "button" | undefined;
    value?: string | readonly string[] | number | undefined;
}

ちなみに、HTMLAttributes<HTMLButtonElement>の場合でも、
以下だとスプレッド構文の特性上通ります。余分な属性も通すためです。

export const Button = ({ children, ...props }: Props) => {
  return <button {...props}>{children}</button>;
};

これだとちゃんとエラーになります。
この辺がややこしいんですよね。。

export const Button = ({ children, type, disabled, ...props }: Props) => {
  return (
    <button {...props} type={type} disabled={disabled}>
      {children}
    </button>
  );
};

(2).ComponentPropsvsComponentPropsWithRef

ComponentPropsの箇所のコメントに書いてありました。
「たいていComponentPropsの代わりにComponentPropsWithRefComponentPropsWithoutRefを使った方が良い。refを許可するかが明確になるから」とのこと。なので、ComponentPropsWithRefを使いましょう。

    /**
     * Used to retrieve the props a component accepts. Can either be passed a string,
     * indicating a DOM element (e.g. 'div', 'span', etc.) or the type of a React
     * component.
     *
     * It's usually better to use {@link ComponentPropsWithRef} or {@link ComponentPropsWithoutRef}
     * instead of this type, as they let you be explicit about whether or not to include
     * the `ref` prop.
     *
     * @see {@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/componentprops/ React TypeScript Cheatsheet}
     *
     * @example
     *
     * ```tsx
     * // Retrieves the props an 'input' element accepts
     * type InputProps = React.ComponentProps<'input'>;
     * ```
     *
     * @example
     *
     * ```tsx
     * const MyComponent = (props: { foo: number, bar: string }) => <div />;
     *
     * // Retrieves the props 'MyComponent' accepts
     * type MyComponentProps = React.ComponentProps<typeof MyComponent>;
     * ```
     */
    type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
        JSXElementConstructor<infer Props> ? Props
        : T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
        : {};

こちらの記事を参照、と書いてあるので見ると

https://react-typescript-cheatsheet.netlify.app/docs/react-types/componentprops/

以下のようである。

React 19+:

ComponentPropsWithRef<ElementType>が推奨
理由:関数コンポーネントでrefがpropsとして渡されるようになった

React ≤18:

refを転送する場合:ComponentPropsWithRef<ElementType>
refを転送しない場合:ComponentPropsWithoutRef<ElementType>

react 18の部分は、以下のような気がする。

React ≤18:

refを転送する(シンプル):ComponentPropsWithRef
refを転送する(forwardRef):ComponentPropsWithoutRef + forwardRef
refを転送しない:ComponentPropsWithoutRef

ちなみにComponentPropsComponentPropsWithRefのエイリアスらしい。

ComponentProps<ElementType> は、要素のすべての有効なプロパティまたはコンポーネントの推論されたプロパティを持つ型を構築します。これは ComponentPropsWithRef<ElementType> の別名です。

(3).〇〇HTMLAttributesvsComponentPropsWithoutRef

この2つはほぼ同じなのでは??と継承元を辿ってみる

ButtonHTMLAttributes

ButtonHTMLAttributes -> HTMLAttributes

ComponentPropsWithoutRef

ComponentPropsWithoutRef -> ComponentProps -> JSX.IntrinsicElements -> DetailedHTMLProps

コードが難解すぎる。。

type ComponentPropsWithoutRef<T extends ElementType> = PropsWithoutRef<ComponentProps<T>>;

// ↓↓↓↓↓↓↓↓↓↓↓

type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
JSXElementConstructor<infer Props> ? Props
: T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
: {};

// ↓↓↓↓↓↓↓↓↓↓↓

interface IntrinsicElements {
    // HTML
    a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
    button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
    ~~~
}

// ↓↓↓↓↓↓↓↓↓↓↓

type DetailedHTMLProps<E extends HTMLAttributes<T>, T> = ClassAttributes<T> & E;

HTMLAttributesが出てきたので、同じになりそう。
自分の実力だと完全に読み切ることは出来ず、、、AIの力を借りた所、
どうやらこの2つは同じ型になるらしい。

AIを完全に信頼出来ないので、どなたかTS得意な方教えてください。

含まれる内容の比較表

基本属性 要素固有属性 ref
❶.HTMLAttributes
❷.ButtonHTMLAttributes
❸.ComponentPropsWithoutRef(❷とほぼ同じ?)
❹.ComponentPropsWithRef
❺.ComponentProps(❹のエイリアス)

実践的な使い分け

(1). 基本❹を使う
(2). refを禁止したい時は❸を使う。❷と❸は型が同じになるらしいが公式推奨の❸で統一で問題なさそう。
(3). ❹と❺は同じだが、refの許可を明確にするために❹を使う
(4). ❶は基本使わない

参考

https://react-typescript-cheatsheet.netlify.app/docs/react-types/componentprops/

https://ttryo.hateblo.jp/entry/2022/04/14/【React_×_TypeScript】HTML属性の型情報のインポート方法

https://qiita.com/honey32/items/958e085f074662f9ef79

宣伝

Next.jsを使ったフロントエンド開発などやってます。
お仕事のご依頼など、はこちらから!

https://www.htmlgo.site/

Discussion