※スマホ対応はしてません。

タグ: TypeScript

Reactで既存HTMLのプロパティを受け取るのにComponentPropsWithRefが使える?

カテゴリー: JavaScript

話題になっているのを全然みかけないんだけど、皆なんかもっと良いやり方やってんの? こういうことしないの?

あとコードハイライトなくてごめんね……。

先にまとめ

  • TypeScript の話
  • 既存 HTML ほとんどそのままのコンポーネントで、本来の props をそのまま受け取りたい場面
  • props: React.ComponentPropsWithRef<'span'> みたいにやれるっぽい
  • ドキュメントには載ってない
  • 定義を追っかけてみたけどたぶんきっと大丈夫そう
  • よくわかってないです

こんな風にして使っている。

export type HtmlProps<T extends ElementType> =
  React.ComponentPropsWithRef<T>;

export type HtmlComponent<T extends ElementType> =
  React.FC<HtmlProps<T>>;

後者は毎回書くのが面倒で追加したけど、ない方がコードは読みやすいかもしれない。

これを使って、特定のクラス名を付けただけのHTML要素を返すコンポーネントの例。

export const NiceButton: HtmlComponent<'button'> = (props) => (
  <button
    {...props}
    className={`NiceButton ${props.className || ''}`}
  />
);
<NiceButton>Niceですね~</NiceButton>

もうちょい複雑なコンポーネントの例。

interface NiceCheckboxProps extends HtmlProps<'input'> {
  label: string;
}

export const NiceCheckbox: React.FC<NiceCheckboxProps> = (props) => {
  const { className = '', label, ...inputProps } = props;
  return (
    <label className={`NiceCheckbox ${className}`}>
      <input {...inputProps} className="NiceCheckbox-input" type="checkbox" />
      <span className="NiceCheckbox-label">{label}</span>
    </label>
  );
};
<NiceCheckbox
  checked={agreed}
  label="ナイスであることに同意します"
  onClick={onAgreeClick}
/>

場面

ほぼ HTML

上に書いた、ああいう「ほとんどただの HTML 要素なんだけどちょっと追加とか組み合わせとかがあって、方々で使うから名前を付けて簡単に使いまわしたい」ような場面です。

CSS フレームワークみたいなノリだと思うんだけど、React でやるからには毎回クラス名書くんじゃなくて、そのクラス名を付けてくれるコンポーネントを使うようにしたいなと。

HTML同様の型が欲しい

例えば <input> だと value を持ってたりするじゃないですか。<span> にはないじゃないですか。そういうのを、補完で出したり型確認で弾いたりしたいなと思っておりました。

見つけた

React.prop まで入力したところの補完で出てきた候補の中からそれっぽいのを眺めてるときに見つけました。

React 公式サイトで検索しても出てこない、と思ったけど React プロジェクトじゃなくて @typed のだから当たり前か。ありがてえ。

定義を追ってみる

ここから先はTS初級者がうんうん唸ってるだけです。誰かに教えてほしい……。

なお @types/jest のバージョンは 24.0.12 でした。

ComponentPropsWithRef

type ComponentPropsWithRef<T extends ElementType> =
    T extends ComponentClass<infer P>
        ? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
        : PropsWithRef<ComponentProps<T>>;

へ、へえー……。

ElementType

まずジェネリクスの方。

type ElementType<P = any> =
    {
        [K in keyof JSX.IntrinsicElements]: P extends JSX.IntrinsicElements[K] ? K : never
    }[keyof JSX.IntrinsicElements] |
    ComponentType<P>;

これはあれだな、IntrinsicElements にあればその型('a' なら <a> 用の props)を使うぞってことだな。

IntrinsicElements

interface IntrinsicElements {
    // HTML
    a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
    abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
    address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
    area: React.DetailedHTMLProps<React.AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>;
    article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
…

究極的にはここの右側 React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> を毎回コピペして使えば使えるんだな。したくないけど。

JSX で書いた <span> とかを VS Code で Ctrl + クリックすると出てくるのもこいつ。

ComponentClass

こっちがわからない。

interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
    new (props: P, context?: any): Component<P, S>;
    propTypes?: WeakValidationMap<P>;
    contextType?: Context<any>;
    contextTypes?: ValidationMap<any>;
    childContextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

<infer P> との組み合わせで、React.Component 継承のクラスを与えると良い感じに props の型を抽出してくれる系かなと思ったんだけどそんなことなかった。

ちなみに infer ってここ(上の ComponentPropsWithRef のとこ)で初めて見ました。

PropsWithRef

Indent Hadouken みがある。

/** Ensures that the props do not include string ref, which cannot be forwarded */
type PropsWithRef<P> =
    // Just "P extends { ref?: infer R }" looks sufficient, but R will infer as {} if P is {}.
    'ref' extends keyof P
        ? P extends { ref?: infer R }
            ? string extends R
                ? PropsWithoutRef<P> & { ref?: Exclude<R, string> }
                : P
            : P
        : P;

たぶん大丈夫でしょう

知らんけど。

とりあえず動いてはいる。うーんちゃんと理解して使いたい……。

おしまい

VS Code で F12 を駆使して追っかけてみました。ほんとべんり。

よくわからないけどとりあえず動いているっぽいのでこのまま使おう。趣味プロジェクトだし、まあーそのうちTS力が上がれば完全理解できるでしょう。

あとそういえばこのブログテーマも更新して、TypeScript のハイライトも入れたいなあ。

@ ts-checkを付けると、VS Codeが普通のJavaScriptもTypeScript扱いで見てくれるんだって。

カテゴリー: JavaScript

// @ts-check を書くと、JSファイルでもTSとしてvalidateしてくれるそうです。

(@tsのひとごめん。)

DOMノードどーすんの問題

引数もらって計算して結果を返すだけの関数ってならこれすげー便利で良いんだけど、 querySelector() 使うと対応が面倒な感じになる。これどうしたら良いかなあ。というかTSでもよく分からないや……。

const input = document.querySelecter('input#the-input');
console.log(input!.value); // -> Property 'value' does not exist on type 'Element'.

querySelecter() が返すのは Element 型なので、 <input> 用のプロパティが定義されていない。

一応、解決策としては instanceof で確認してやることで型が固定します。

const input = document.querySelecter('input#the-input');
if (!(input instanceof HTMLInputElement)) { return; }
console.log(input!.value);

ついでに null チェックもできて一石二鳥ではある。

JSじゃなくてTSの場合はジェネリクス <T> で書ける。

const input = document.querySelector<HTMLInputElement>('input#the-input')!;
input.value = 'Hello World!';

ただしこの場合は null の確認が必要。(これ↑は ! ですっ飛ばした。)

なんかもっと楽々書けないかなあ。そもそもReactやVueが全盛期の現代で querySelector() 自体をあんまり使わないか……。