Reactのコンポーネントでジェネリクスで型を指定してpropsの値を制限したい

はじめに

通常でもpropsに型を指定して、記述できる内容を制限していますが、
状況により、制限する内容を変えたい場合、ジェネリクスで指定できれば見た目にも分かりやすくなります

今回作るもの

今回の要件として、リンクのコンポーネントを用意します

種類リンク先リンクテキスト
IR「/ir」から始まる文字列制限無し
お知らせ「/info」から始まる文字列「お知らせ:」から始まる文字列
外部リンク「https://www.xx.co.jp」から始まる文字列制限無し

リンクの型を作成

// 基本
interface BaseLinkType {
  path: `https://${string}` | `/${string}`
  text: string
}

// IR
export interface Ir extends BaseLinkType {
  path: `/ir/${string}`
}

// お知らせ
export interface Information extends BaseLinkType {
  path: `/info/${string}`;
  text: `お知らせ:${string}`
}

// サービスサイト
export interface Service extends BaseLinkType {
  path: `https://www.xxxx.co.jp/${string}`
}

コンポーネントにジェネリクスの型を適用する

import React, { PropsWithChildren } from 'react';

type Link = Ir | Information | Service

interface Props<T extends Link> {
  path: T['path']
  text: T['text']
}

export const Link = <T extends Link>({
  path,
  text
}: PropsWithChildren<Props<T>>): React.ReactElement => {
  return (
    <a href={path}>{text}&l;/a>
  )
}
これで、ジェネリクスを指定したLinkコンポーネントは完成です
Linkコンポーネントを利用する側の実装例です
import React from 'react'
import { Link, type Information } from './Link'

export const Test: React.FC = () => {
  return (
    <Link<Information>
      path="/info/20230412"
      text="お知らせ:XX店オープン"
    />
  )
}

リファクタリング

ジェネリクスの指定を、文字列にした方が、型を export / import しなくてよくなります
type Link = {
  ir: Ir
  info: Information
  service: Service
}

interface Props<T extends keyof Link> {
  path: Link[T]['path']
  text: Link[T]['text']
}

export const Link = <T extends keyof Link>({
  path,
  text
}: PropsWithChildren<Props<T>>): React.ReactElement => {
  return (
    <a href={path}>{text}</a>
  )
}
このようにすると、各Linkの型のexportも不要になり、利用する側も少しですが記述する量が少なくなります
import React from 'react'
import { Link } from './Link'

export const Test: React.FC = () => {
  return (
    <Link<'info'>
      path="/info/20230412"
      text="お知らせ:XX店オープン"
    />
  )
}

リファクタリング後のコンポーネント全体のコード

import React, { PropsWithChildren } from 'react';

// 基本
interface BaseLinkType {
  path: `https://${string}` | `/${string}`;
  text: string;
}

// IR
interface Ir extends BaseLinkType {
  path: `/ir/${string}`;
}

// お知らせ
interface Information extends BaseLinkType {
  path: `/info/${string}`;
  text: `お知らせ:${string}`
}

// サービスサイト
interface Service extends BaseLinkType {
  path: `https://www.xxxx.co.jp/${string}`;
}

type Link = {
  ir: Ir
  info: Information
  service: Service
}

interface Props<T extends keyof Link> {
  path: Link[T]['path']
  text: Link[T]['text']
}

export const Link = <T extends keyof Link>({
  path,
  text
}: PropsWithChildren<Props<T>>): React.ReactElement => {
  return (
    <a href={path}>{text}</a>
  )
}