TypeScript と共に使う時に className にリテラル型のユニオンを設定する

jsx で何の気なしにclassName属性を使っている時の型はReact.HTMLAttributesの中にclassName?: stringと定義(@types/react@16.9.2では@types/react/index.d.ts:1628辺り)されています。

また例えば<div>タグを使った時の型はdiv: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;と定義(JSX.IntrinsicElements@types/react/index.d.ts:2858辺り)されています。

ということで、実質React.HTMLAttributesが使われているということで、それを自分で上書きしてあげれば良さそうに感じますがdelare module 'react'で定義したとしても既存のプロパティの解決はデフォルトのもの(@types/react)が優先して使われるようなので無理です。

ではどうするかと言うと、JSX.IntrinsicElementsからすべて再定義してあげます。まずは既存のJSX.IntrinsicElementsをすべてコピーしdelare module 'react'内に置きます。

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      a: React.DetailedHTMLProps<
        React.AnchorHTMLAttributes<HTMLAnchorElement>,
        HTMLAnchorElement
      >;
    }
  }
}

あとはこのReact.AnchorHTMLAttributesを更に再定義してあげてあげれば目的は果たせそうです。ですがReact.AnchorHTMLAttributesaの場合だけで、tableだったらReact.TableHTMLAttributesinputだったらReact.InputHTMLAttributesだったりと数が多くすべてに対応していたらそれだけで一苦労です。

TypeScript には、

interface A {
   foo?: string;
}

interface B {
   foo: 'foo'
}

type C = A & B;
// C = {foo: 'foo'};

になるという法則があります。そしてすべてのReact.*HTMLAttributesReact.HTMLAttributesを拡張したものなので、React.HTMLAttributesを継承したものをジェネリクス型として受け取れる何か型を定義し、{className: 自分の型;}とすればいけそうです。

declare module 'react'の中にこのようなものも定義してみました。

type MyHTMLAttributes<T extends React.HTMLAttributes> = T & {
  className?: 'foo' | 'bar';
}

やっていることは前のコードと同じstring | undefined'foo' | 'bar'へ変換することです。あとはすべてのReact.*HTMLAttributesMyHTMLAttributesで囲めば目的が果たせそうです。

VSCode 以外はできるか分かりませんが、できるなら以下の組み合わせで一気に変換すると楽かなと思います。


\s(React.*HTMLAttributes(?!>)<[^,]*)
# から
 MyHTMLAttributes<$1>
# 先頭にスペース注意