話題になっているのを全然みかけないんだけど、皆なんかもっと良いやり方やってんの? こういうことしないの?
あとコードハイライトなくてごめんね……。
先にまとめ
- 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
/** 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 のハイライトも入れたいなあ。