ジェネリックなコンポーネントの型注釈
ジェネリックじゃないコンポーネントなら、こんなふうに、const宣言に型を書くだけで、引数と戻り値の型まで推論してくれる。しかもchildrenまで推論してくれているので宣言しなければいけない箇所が減る。
const Comp: React.FC<{value: string}> = ({value, children}) => {
return <div></div>
}
しかし、コンポーネントにジェネリクスを付けた途端にそのような宣言は出来なくなる。
型変数T
を宣言したあとでしかT
を使うことが出来ないからだ。
引数と戻り値にわざわざ型注釈を付けて、覚えるまではReactの型定義を見て正しい型名を探さないといけない。美しくないし。
interface Props<T extends string>{ // Props<"apple" | "banana">というふうに使える
values: Record<T, string>, // 例: { apple: "りんご", banana: "バナナ" }
initial: T
}
const Comp = <T extends string>(prop: PropsWithChildren<Props<T>>): ReactElement => {
return <div></div>
}
const App = () => <Comp values={{apple: "Apple"}} initial="apple" />
高カインド型の導入
そこで、活用できるのが**Higher Kinded Type(高カインド型)**である。React.FC<型>
としていたところを、GenericFC<カインド>
にすることで、型引数を受け取れるようにできるはずだ。やはりScalaをやっておいてよかった。
TypeScriptで高カインド型(Higher kinded types)--Qiita
を参考にして実装してみたが…
interface HKT<T> {} // globalで宣言する
export type GenericFC<K extends keyof HKT<any>> = <T>(
...args: Parameters<React.FC<HKT<T>[K]>>
) => ReturnType<React.FC<HKT<T>[K]>>;
import { GenericFC } from "./generic-fc"
declare module "./generic-fc" {
interface HKT<T> {
props: Props<T> // ここで"props"カインドを定義
}
}
interface Props<T extends string>{
values: Record<T, string>,
initial: T
}
const Comp: GenericFC<"props"> = (props) => { //ここでカインドを使っている
return <div></div>
}
const App = <Comp values={{apple: "Apple"}} initial="apple" />
(シンタックスハイライトがおかしくなるので2つに切りました。)
Record<T, string>
型のT
はオブジェクトのキーなのでstring | number | symbol
型でないとダメで、今回はT extends string
としているが…
HKT<T extends string>
としないと、このように型の制約を満たさなくなってしまうが、もしそうしてしまうと、他で定義するすべての同様のジェネリクスに影響してしまう。
どうにかしてこの個別のProp型だけに型制約を付けるかと考えるたすえ、このようにすると成功した。
interface HKT<T> {}
type GenericFC<K extends keyof HKT<any>> = <T>(
...args: Parameters<React.FC<HKT<T>[K]>>
) => ReturnType<React.FC<HKT<T>[K]>>;
import { GenericFC } from "./generic-fc"
declare module "./generic-fc" {
interface HKT<T> {
props: T extends string ? Props<T> : never
}
}
interface Props<T extends string>{
values: Record<T, string>,
initial: T
}
const Comp: GenericFC<"props"> = (props) => {
return <div></div>
}
const App = <Comp values={{apple: "Apple"}} initial="apple" />
const ShouldFail = Comp<number>({}) //これがちゃんとエラーになる
T extends string ? Props<T> : never
のところは Conditional Typesになっていて、T extends string
のときのみHKT<T>["prop"]
からProp<T>
得られるが、そうでないときにはnever
型となり、意図どおりの型エラーが起きてくれる。
めでたしめでたし。