Astro で一部がポリモーフィックなコンポーネントを作る

## この記事の概要

Astro にはデフォルトでポリモーフィックなコンポーネントを作成するための型があります。

```astro:公式ドキュメントにある例
---
import type { HTMLTag, Polymorphic } from 'astro/types';

type Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag }>;

const { as: Tag, ...props } = Astro.props;
---

<Tag {...props} />
```

https://docs.astro.build/en/guides/typescript/#polymorphic-type

ただ、コンポーネント全体がポリモーフィックではなく（？）一部だけがポリモーフィック（？）な場合の説明がなかったので、記事にしてみました。

:::note warn
言葉の使い方として正しいのかいまいち自信が無いのですが、ニュアンスは伝わるはず……と思っています。ご容赦ください。
:::

## やりたいこと

`article`の情報を受け取って、リンク先や記事タイトルなどをそれぞれ適用するコンポーネントを考えます。

最終的な HTML はこのようなイメージです。

```html
<a href="記事へのリンク">
  <h2>記事のタイトル</h2>
  <time datetime="投稿日時">投稿日時</time>
</a>
```

ただ、見た目がまったく同じであっても、ページの構成によってはタイトル部分を`h3`にしたい、などがあると思います。

というわけで、コンポーネント全体ではなくタイトルを囲うタグだけが可変するように作りたい、とします。

## 環境

astro@2.5.0 以上であれば問題ありません。

また、この記事を執筆している時点の最新バージョンは [4.11.1](https://github.com/withastro/astro/releases/tag/astro%404.11.1) です

## 条件

- コンテンツコレクションを使用している
    - コレクション名は`article`
    - 独自のスキーマは`title`と`updatedAt`
- タイトルとして受け取るタグは`h`タグのどれか

## 完成形 1

```astro
---
import type { Polymorphic } from "astro/types";
import type { CollectionEntry } from "astro:content";

type Props<Tag extends "h1" | "h2" | "h3" | "h4" | "h5" | "h6"> = {
  article: CollectionEntry<"article">;
} & Polymorphic<{ as: Tag }>;

const { article, as: Tag } = Astro.props;
---

<a href={`article/${article.slug}`}>
  <Tag>
    {article.data.title}
  </Tag>
  <time datetime={article.data.updatedAt.toISOString()}>
    {article.data.updatedAt.toLocaleDateString("ja-JP")}
  </time>
</a>
```

やっていることは以下です。

- `Polymorphic`を利用して可変なタグを作成する
- `CollectionEntry`を利用して`article`コレクションの型を取得する
- 2 つの型をあわせて全体の`Props`とする

もともとあるものを活かせているのでこれで良いような気がするのですが、若干気になる点があります。

`Astro.props`の中身（今回でいうと`article`）が`any`扱いになってしまうのです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/e2af5eb6-77a2-6533-3963-a01263bd0f77.png)

## 完成形 2

先ほどのコードのうち、frontmatter 部分だけを変えます

```diff
  ---
- import type { Polymorphic } from "astro/types";
  import type { CollectionEntry } from "astro:content";

- type Props<Tag extends "h1" | "h2" | "h3" | "h4" | "h5" | "h6"> = {
-   article: CollectionEntry<"article">;
- } & Polymorphic<{ as: Tag }>;
+ type Props = {
+   article: CollectionEntry<"article">;
+   titleTag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
+ };

- const { article, as: Tag } = Astro.props; 
+ const { article, titleTag: Tag } = Astro.props;
  ---

  <a href={`article/${article.slug}`}>
    <Tag>
      {article.data.title}
    </Tag>
    <time datetime={article.data.updatedAt.toISOString()}>
      {article.data.updatedAt.toLocaleDateString("ja-JP")}
    </time>
  </a>
```

コンポーネントの機能自体は変わりませんが、型が見やすくはなりました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/18e25a4c-9eb6-d0cf-56f8-e3ae131d6c1b.png)

余談ですが`Polymorphic`では`as: Foo`という名前しか使えません。

今回の場合は「コンポーネントの一部が変化する」というのもあり`as`という単語も少し変な気がして`titleTag`と名前を変えました。

## 使い方

これで、このように使い分けられるようになりました

```jsx:Astroでのコード
<PolymorphicLink article={article1} titleTag="h2" />
<PolymorphicLink article={article2} titleTag="h3" />
```

```html:アウトプットされたHTMLのイメージ
<a href="article1へのリンク">
  <h2>article1のタイトル</h2>
  <time datetime="article1の投稿日時">article1の投稿日時</time>
</a>
<a href="article2へのリンク">
  <h3>article2のタイトル</h3>
  <time datetime="article2の投稿日時">article2の投稿日時</time>
</a>
```