Closed5

柔軟にタグを指定できる Button.tsx コンポーネントに型をつけたい

Ryosuke MiyamotoRyosuke Miyamoto

欲しい仕様

  1. as で HTML タグを指定できる。デフォルトは button にしたい。また onClick などHTMLが持つ attributes は props で毎回指定したくない。
      <Button className="mt-2"  as="button" onClick={onClick}>
        ボタンテキスト
      </Button>

      <Button className="mt-2"  as="a" onClick={onClick}>
        ボタンテキスト
      </Button>
  1. ただし、as で指定できる HTML タグを a | button のみ制限したい
   // as にエラーついて欲しい
      <Button className="mt-2"  as="div" onClick={onClick}>
        ボタンテキスト
      </Button>
  1. as のHTMLタグで使用できない attributes はエラーになって欲しい
     // href にエラー欲しい
      <Button className="mt-2" href="https://zenn.dev" as="button" onClick={onClick}>
        ボタンテキスト
      </Button>

   // href はエラー無し
      <Button className="mt-2" href="https://zenn.dev" as="a" onClick={onClick}>
        ボタンテキスト
      </Button>
  1. できれば、as に "next/Link" も nextjs の component も指定したい
      <Button className="mt-2" href="https://zenn.dev" as="Link" onClick={onClick}>
        ボタンテキスト
      </Button>
Ryosuke MiyamotoRyosuke Miyamoto

ここまで button.tsx 書いたけど、Tag の部分でエラー出ちゃう。

誰かぁ〜。わかんないよ〜。教えてん

import classnames from 'classnames'
import { ReactNode } from 'react'

type Tags = 'button' | 'a'
type Props<T extends Tags> = JSX.IntrinsicElements[T] & {
  as?: T
  children?: ReactNode
  className?: string
}

export const Button = <T extends Tags = 'button'>({ as, children, className, ...props }: Props<T>) => {
  const Tag = as || 'button'
  return (
    <Tag className={classnames(`btn`, className)} {...props}>
      {children}
    </Tag>
  )
}

以下、TS plauground
https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAYwDYEMDOaB2KQFM1wBmUEIcA5MuljvuQFCiSxwDecASrigjAHIQAJrjgBfIiTLko3Xg3owAnmBEAVFAHMCAXgoAjAK4wYETOTgAfCigZKVcAAokwaADyq4uAB4xcmQQTqWgB8cLoAUgDKABoAdACSmDBQwJhowAgAoki4eEloANqqALpwAGRs9HBw6AD8AFxwqlWIABbASIIymA2csvxCuC1UGHy0vWjJqRr0ovT03szwCKaTcABCRiaYYXDunj5+AU2aOvpbpuTBABTs6AA0bR1dfo8jaGN4j7E-YM4EokaTggLncwQAlGFQqxhqt4EFduhLFZyIZjJcWjIYAYoDtri1qu5NIhUKNaNpWO9sHg0NcAAZ6GCYOlvUkfWjg8SsH6xP4gtCiYIE6psBDtTrdOYivYAeiCQuq4Nm9CAA

Ryosuke MiyamotoRyosuke Miyamoto

createElement を使うとエラーでなくて、 1-3 までの仕様は実装できてる(気がする)が、なんで?

import classnames from 'classnames'
import { createElement, ReactNode } from 'react'

type Tags = 'button' | 'a'
type Props<T extends Tags> = JSX.IntrinsicElements[T] & {
  as?: T
  children?: ReactNode
  className?: string
}

export const Button = <T extends Tags = 'button'>({ as, children, className, ...props }: Props<T>) => {
  return createElement(
    as || 'button',
    {
      ...props,
      class: classnames(`btn`, className),
    },
    children
  )
}

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAYwDYEMDOaB2KQFM1wBmUEIcA5MuljvuQFCiSxwDeiUuKMuAokrjyYYAGjgAlLghgA5CABNccAL5ESZcpxTSG9GAE8wSgCooA5gQC8FAEYBXGDAiZycAD4UUDA0bgAFEjA0AB5jOFwADx5MeQJTCwA+OGsAKQBlAA0AOgBJYShgTDRgBH5BXGE0AG1jAF04ADI2ejg4dAB+AC44YxbEAAtgJHlOTC6JKVkFXD6qDBlacbQYAswzemV6ekjmeARnZbgAIQcnTGS4UPCoitie8ytbU+dyBIAKdnQxBEHh0e-UPNaGIsqCwIECMpugEIEFQgkAJTJJKsPqcGB2KDnBBaHhlIQwN59Vrodwecj2RwvETE5qtelwUFZcGwtA0hmtOZobpc7B4NBvAAGNhgmEFAOoCzwCPZ9OUss5vxGFT6CI29CAA

Ryosuke MiyamotoRyosuke Miyamoto

できたかも?!!!

Link タグで囲うのを、単にゴリ押して分岐させてるけど、もっと cool な書き方ないんかな?

button.tsx
import classnames from 'classnames'
import Link, { LinkProps } from 'next/link'
import { createElement, ReactNode } from 'react'

type Tags = 'button' | 'a'

type Element<T extends Tags | 'Link'> = T extends Tags
  ? JSX.IntrinsicElements[T]
  : LinkProps & JSX.IntrinsicElements['button']

type Props<T extends Tags | 'Link'> = Element<T> & {
  tag?: T | 'Link'
  children?: ReactNode
  className?: string
}

export const Button = <T extends Tags | 'Link'>({ tag, size, color, children, className, ...props }: Props<T>) => {
  const attrs = {
    ...props,
    class: classnames(`btn`, className),
  }
  const tagName = tag === 'Link' ? 'a' : tag || 'button'
  return tag === 'Link' ? (
    <Link href="/" {...props}>
      {createElement(tagName, attrs, children)}
    </Link>
  ) : (
    createElement(tagName, attrs, children)
  )
}
このスクラップは2022/05/01にクローズされました