2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React×TS】ジェネリックなコンポーネントの型定義をしようとしたら高カインド型にぶつかった

Last updated at Posted at 2020-10-28

ジェネリックなコンポーネントの型注釈

ジェネリックじゃないコンポーネントなら、こんなふうに、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
を参考にして実装してみたが…

generic-fc.d.ts
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]>>;
comp.tsx
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型だけに型制約を付けるかと考えるたすえ、このようにすると成功した。

generic-fc.d.ts
interface HKT<T> {}

type GenericFC<K extends keyof HKT<any>> = <T>(
    ...args: Parameters<React.FC<HKT<T>[K]>>
) => ReturnType<React.FC<HKT<T>[K]>>;
comp.tsx
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型となり、意図どおりの型エラーが起きてくれる。

めでたしめでたし。

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?