ヨンソクのブログ
戻る
4 min read
HTML element attributesについて調べてみよう

この発表の内容の一部を補足して整理してみた。

React.HTMLAttributes<HTMLButtonElement>

React.HTMLProps<HTMLButtonElement>

JSX.IntrinsicElements['button']

React.ButtonHTMLAttributes<HTMLButtonElement>

React.ComponentProps<'button'>

React.ComponentPropsWithRef<'button'>

React.ComponentPropsWithoutRef<'button'>

上記の多くの型… これらはすべてbuttonに対する型を表している。
作業していると、たまにこれらを拡張して使う必要があるケースに出くわすが、どれを使えばいいのだろうか?
一つずつ見ていこう。

1. React.HTMLAttributes<HTMLButtonElement>

interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
  // React-specific Attributes

  // Standard HTML Attributes

  // RDFa Attributes (RDFaについてはまた別の機会に書いてみよう)
}

interface DOMAttributes<T> {
  children?: ReactNode | undefined;
  // ...
  // event handlers...
}

ジェネリクスで受け取り、DOMAttributesに拡張される。
全体的に多くのHTML属性を含んでいるが、特定のタグに対する具体的な型は提供できない。

interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {}

function Button(props: ButtonProps) {
  // ❌ エラー: 'type'プロパティはHTMLButtonElementに存在しません。
  const isSubmit = props.type === 'submit';
  // ...
}

2. React.HTMLProps<HTMLButtonElement>

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/2bcc7e759889c24eff2891949c18dcabfc38d888/types/react/index.d.ts#L2302

interface HTMLProps<T> extends AllHTMLAttributes<T>, ClassAttributes<T> {
}

interface AllHTMLAttributes<T> extends HTMLAttributes<T> {
  //...
  type?: string | undefined;
  // ...
}
interface ClassAttributes<T> extends RefAttributes<T> {
}

AllHTMLAttributesのエイリアスとして、すべてのHTML属性を含んでいる。
そのため型自体も多く、特定のタグに対する型も提供されるが、あまり具体的でない型になってしまう。

interface ButtonProps extends React.HTMLProps<HTMLButtonElement> {}

function Button(props: ButtonProps) {
  // ❌
  // type?: 'button' | 'submit' | 'reset' | undefined;
  //
  // 'type'プロパティは存在するが、型が不十分です。
  // type?: string | undefined;
  const type = props.type;
  // ...
}

3. JSX.IntrinsicElements['button']

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

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

TypeScriptのグローバルスコープにある型で、使用可能なネイティブJSXエレメントの型を提供する。
そのため「組み込みエレメント」とも呼ばれる。\

しかし、この型はインターフェースの拡張ができない。

// ❌ エラー: インデックスアクセス型は拡張できません。
interface ButtonProps extends JSX.IntrinsicElements['button'] {}

こうすれば一応できるが..

interface ButtonProps extends NonNullable<JSX.IntrinsicElements['button'], {}> {}

4. React.ButtonHTMLAttributes<HTMLButtonElement>

interface ButtonHTMLAttributes<T> extends HTMLAttributes<T> {
    disabled?: boolean | undefined;
    form?: string | undefined;
    formAction?:
        | string
        | 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;
}

上記のJSX.IntrinsicElements['button']の内部で確認できた型で、 私たちが必要とするものを提供してくれる。ただし、もっと便利な型がある。

ここに非常に面白い型がある。
DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS
RSC(React Server Components)でform actionをサポートするための型だ。
現在Canaryチャンネルでのみ使用可能だ。型名の通り、今使うとクビになるかもしれない。

ここでは具体的に掘り下げないが、以下の資料を見ればより詳しく知ることができるだろう。

5. React.ComponentProps<'button'>

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

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

まず結論から言うと、ButtonHTMLAttributesと同等と言えるが、より短く簡潔だ。使わない理由がない。

型が少し複雑だが、順を追って見ていこう。

  • 第一条件: T extends JSXElementConstructor<infer P>TJSXElementConstructor<infer P>に割り当て可能か?
    つまりTがコンポーネントコンストラクタ型であれば、コンストラクタが受け取るprops型であるPを返す。
  • 第二条件: 第一条件を満たさず、T extends keyof JSX.IntrinsicElementsTJSX.IntrinsicElementsに割り当て可能であれば、
    つまりTJSX.IntrinsicElementsのキーの一つであれば、JSX.IntrinsicElements[T]を返す。

React.ComponentProps<'button'>はJSXエレメントを生成する関数やクラスの型ではないため、第二条件に該当する。 そのためJSX.IntrinsicElements['button']を返す。

JSX.IntrinsicElements['button']はさらにReact.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>を返す。 結局、必要なものをすべて含んでいる。

第一条件に該当するケースは以下の通りだ。

type Props = { name: string; age: number; };
type Component = (props: Props) => (<div>...</div>);
type ComponentProps = React.ComponentProps<typeof Component>;
// ComponentProps = { name: string; age: number; }

この構造により、柔軟に使用できる。

6. React.ComponentPropsWithRef<'button'>, React.ComponentPropsWithoutRef<'button'>

type ComponentPropsWithRef<T extends ElementType> = T extends (new(props: infer P) => Component<any, any>)
    ? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
    : PropsWithRef<ComponentProps<T>>;

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

type PropsWithoutRef<P> =
    P extends any ? ("ref" extends keyof P ? Omit<P, "ref"> : P) : P;
type PropsWithRef<P> =
    "ref" extends keyof P
        ? P extends { ref?: infer R | undefined }
            ? string extends R ? PropsWithoutRef<P> & { ref?: Exclude<R, string> | undefined }
            : P
        : P
        : P;

React.ComponentPropsWithRef<'button'>

こちらも型が少し複雑だが、順を追って見ていこう。

  • 第一条件: T extends (new(props: infer P) => Component<any, any>)Tnew(props: infer P) => Component<any, any>に割り当て可能か?
    Tがクラスコンポーネントのコンストラクタに割り当て可能であれば、props型からrefを除いた残りの型とRefAttributes<InstanceType<T>>を合わせた型を返す。
  • そうでなければ、PropsWithRef<ComponentProps<T>>を返す。

PropsWithRefはさらに複雑だが、簡単にまとめると、refが文字列型でない場合に該当するrefの型を維持または調整して返す。

結論

React.ComponentProps<'button'>を使えば、簡潔かつ柔軟に使用できる。