【React】【TypeScript】データ属性を含めてHTML要素の属性の型を定義する

 とりあえず↓の型を使えばいい感じに動いてくれます。

type HTMLElementProps = JSX.IntrinsicElements &
  Record<keyof JSX.IntrinsicElements, { [p: `data-${string}`]?: string | number }>;

// 使用例
const BoxWrapper: React.FC<{ divProps: HTMLElementProps['div'] }> = (props) => {
  return <div {...props.divProps} >aaa</div>;
};

const App = () => {
  return <BoxWrapper divProps={{ 'data-hoge': 'fuga' }} />;
};

 以下は上記の型とその型で何をしたかったのかの説明です。

 React のコンポーネントを定義する時、次の様に子要素に透過的に props を定義できるようにすることがしばしばあります。この方法はコンポーネントとして何かをまとめても、まとめた何かの機能を損なうことがないのが便利です。

const BoxWrapper: React.FC<{ boxProps: React.ComponentProps<typeof Box> }> = (props) => {
  // Box の props 全体を扱える boxProps を BoxWrapper の props として定義する
  return <Box {...props.boxProps} />;
};

const App = () => {
  // BoxWrapper の props の定義上で Box の方も完全に定義できる
  return <BoxWrapper boxProps={{ width: '20px' }} />;
};

 この透過的な props を定義する時、React のコンポーネントならば例の様に React.CompoentProps<typeof コンポーネント名> とすれば props 全体を定義できます。しかしながら HTML 要素の場合、そうは行きません。React.ComponentProps<typeof div>React.ComponentProps<typeof HTMLDivElement>の様に書いてもエラーにしかなりません。もし HTML 要素に対して同様のことを行う場合は次の様にJSX.IntrinsicElementsを使うのが楽です。

// React 組み込みの型の JSX.IntrinsicElements には HTML 要素の Props 定義が詰まっています。
const BoxWrapper: React.FC<{ divProps: JSX.IntrinsicElements['div'] }> = (props) => {
  // div の props 全体を扱える boxProps を BoxWrapper の props として定義する
  return <div {...props.divProps} />;
};

const App = () => {
  // BoxWrapper の props の定義上で div の方もおおよそ定義できる
  return <BoxWrapper divProps={{ style: { width: '20px' } }} />;
};

 HTML 要素に対して透過的な props を作りたい場合、概ね上記のみで対応できるのですがデータ属性に対応できていません。

 HTML にはデータ属性というものがあります。

data-* – HTML: HyperText Markup Language | MDN
データ属性の使用 – ウェブ開発を学ぶ | MDN
HTMLElement.dataset – Web API | MDN

 これは上記ページから引用した次のコードの様に HTML の要素にdata-*の属性で値を定義し、それを JavaScript 内で読む仕組みです。

<!-- https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/dataset から引用 -->
<div id="user" data-id="1234567890" data-user="johndoe" data-date-of-birth>John Doe</div>
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/dataset から引用
const el = document.querySelector('#user');

// el.id === 'user'
// el.dataset.id === '1234567890'
// el.dataset.user === 'johndoe'
// el.dataset.dateOfBirth === ''

// データ属性の設定
el.dataset.dateOfBirth = '1960-10-03';
// JS での結果: el.dataset.dateOfBirth === '1960-10-03'
// HTML での結果: <div id="user" data-id="1234567890" data-user="johndoe" data-date-of-birth="1960-10-03">John Doe</div>

delete el.dataset.dateOfBirth;
// JS での結果: el.dataset.dateOfBirth === undefined
// HTML での結果: <div id="user" data-id="1234567890" data-user="johndoe">John Doe</div>

if ('someDataAttr' in el.dataset === false) {
  el.dataset.someDataAttr = 'mydata';
  // JS での結果: 'someDataAttr' in el.dataset === true
  // HTML での結果: <div id="user" data-id="1234567890" data-user="johndoe" data-some-data-attr = "mydata">John Doe</div>
}

 要するに HTML 上に独自のキーと値のペアを設定できる仕組みです。これを利用したライブラリは多くあります。

 先ほどの透過的な props を用いた React コンポーネント上でこのデータ属性を扱おうとすると次の様にエラーとなります。

const BoxWrapper: React.FC<{ divProps: JSX.IntrinsicElements['div'] }> = (props) => {
  return <div {...props.divProps} >aaa</div>;
};

const App = () => {
  // src/App.tsx:14:34 - error TS2322: Type '{ 'data-hoge': string; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
  //   Object literal may only specify known properties, and ''data-hoge'' does not exist in type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
  // 
  // 14   return <BoxWrapper divProps={{ 'data-hoge': 'fuga' }} />;
  return <BoxWrapper divProps={{ 'data-hoge': 'fuga' }} />;
};

 JSX.IntrinsicElements[‘div’] の型に { ‘data-hoge’: ‘fuga’ } を割り当てることができないというエラーです。このままではデータ属性を透過的に渡すことができません。これを解決するために型を拡張することを考えます。

 データ属性にも対応した型が次です。

type HTMLElementProps = JSX.IntrinsicElements &
  Record<keyof JSX.IntrinsicElements, { [p: `data-${string}`]?: string | number }>;

 個々の属性を定義した JSX.IntrinsicElements と JSX.IntrinsicElements の各要素名をキーとしたデータ属性の辞書的オブジェクトの交差型です。次の様に型のプロパティに data-* を追加した感じです。

type HTMLElementProps['div'] = {
    className?: string | undefined;
    style?: CSSProperties | undefined;
     ...
     'data-hoge'?: string | number;
     'data-なんでもOK'?: string | number;
     ...
}

データ属性のキーは TypeScript の型定義機能である Template Literal Types を用いています。これはリンク先にある様に文字列の型に部分的に型を持ち込む仕組みです。データ属性に対応する型の場合はdata-の後に文字列が続く、と定義すればよいので`data-${string}`となります。Template Literal Types は他にも`${number}px`でピクセルの大きさ指定、`${0 | 1}${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | `2${0 | 1 | 2 | 3}`で 00~24の時間表記など様々な使い道があります。

TypeScript: Documentation – Template Literal Types

 このデータ属性にも対応した型を次の様に使うことで TypeScript と React でコーディングがより快適になります。

type HTMLElementProps = JSX.IntrinsicElements &
  Record<keyof JSX.IntrinsicElements, { [p: `data-${string}`]?: string | number }>;

// 使用例
const BoxWrapper: React.FC<{ divProps: HTMLElementProps['div'] }> = (props) => {
  return <div {...props.divProps} >aaa</div>;
};

const App = () => {
  return <BoxWrapper divProps={{ 'data-hoge': 'fuga' }} />;
};
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG