同名の `@utility` を複数定義して値の型ごとに使い分けられるように

tailwindlabs/tailwindcss

CSS の @utility に同じ名前で異なる値型のハンドラーを複数定義した場合、最初のハンドラーが null を返すと後続のハンドラーが試されずコンパイルが止まっていた問題が修正されました。これにより、foo-red-500(カラー)と foo-123(数値)のように同名のユーティリティクラスを値の型ごとに定義して共存させることができます。

背景

@utility foo-* を同じ名前で複数定義し、それぞれ異なる値型を扱うケースは、Tailwind CSS 組み込みの text-red-500(カラー)と text-2xl(フォントサイズ)のように自然なパターンです。しかし #16948 で報告されたように、このアプローチは期待通りに動作しませんでした。

問題の核心は、compile.ts 内でユーティリティのハンドラーをループする際の null の解釈にありました。ハンドラーが「この値は自分の担当ではない」を示すために null を返すと、コンパイルループがそこで完全に終了し、後続のハンドラーに制御が移りませんでした。定義の順序が foo-red-500foo-123 かのどちらが動くかを決定してしまっていたのです。

この挙動は matchUtilities(JSプラグイン)との区別がなかったことが原因です。matchUtilities では型を明示的に指定することで「この型以外は無効」というセマンティクスを持ちますが、CSS の @utility はそのような型オプションを持たないため、異なるセマンティクスが適切です。

技術的な変更

packages/tailwindcss/src/compile.tscompileBaseUtility 関数内、null チェック箇所の挙動が utility.options?.types の有無によって分岐するように変更されました。

変更前:

let compiledNodes = utility.compileFn(candidate)
if (compiledNodes === undefined) continue
if (compiledNodes === null) return asts
asts.push(compiledNodes)

変更後:

let compiledNodes = utility.compileFn(candidate)
if (compiledNodes === undefined) continue
if (compiledNodes === null) {
  // `null` means that the result is invalid and that this plugin should not
  // result in any CSS, but that doesn't mean that subsequent plugins are
  // invalid as well.
  //
  // However, for backwards compatibility with `matchUtilities` this means
  // that we do need to bail entirely: plugins that handle a specific
  // arbitrary value type prevent falling through to other plugins if the
  // result is invalid for that plugin
  if (utility.options?.types?.length) return asts

  continue
}
asts.push(compiledNodes)

変更はシンプルで、null の意味を options.types の有無 で二分しています。types が指定されている場合(matchUtilities 由来)はこれまで通り return asts で即座に終了します。types がない場合(CSS @utility 由来)は continue で次のハンドラーへ進みます。この分岐は compileBaseUtility 内の2つのループブロック(通常ハンドラーと fallback ハンドラー)に同一の変更として適用されています。

追加されたテストでは以下のように同名の @utility を2つ定義し、カラー値と数値の両方が正しくコンパイルされることを検証しています。

@utility foo-* {
  color: --value(--color-*);
}

@utility foo-* {
  font-size: --spacing(--value(number));
}

foo-red-500foo-123 を入力した場合、それぞれ .foo-red-500 { color: var(--color-red-500); }.foo-123 { font-size: calc(var(--spacing) * 123); } が出力されるようになります。

設計判断

null のセマンティクスをハンドラーの種別によって分離するアプローチが採用されました。

null が「無効な値」を示すシグナルであることは変わりませんが、「無効」の帰結が異なります。CSS @utility(型オプションなし)では「このハンドラーには合わないが、次を試す余地がある」と解釈され、matchUtilities(型オプションあり)では「この型では明示的に無効であり、後続も試さない」と解釈されます。後者の挙動はこれまでと同じです。

この設計判断は後方互換性を最優先にしています。matchUtilities を使う既存のJSプラグインにとって、型チェックの厳密さは意図的な機能です。ある型を指定して null を返したハンドラーの後に、別の型を期待する別のハンドラーが動いてしまうと、型安全性の保証が崩れます。utility.options?.types?.length という判定は、オプトアウトではなく既存の動作を明示的に保護するものです。

まとめ

compile.tsnull 処理に1行の条件分岐を加えることで、CSS @utility と JS matchUtilities の意味論の違いを明確に体現しました。@utility による同名マルチハンドラー定義は、Tailwind 組み込みユーティリティと同様のパターンをカスタムユーティリティでも実現できるようになり、matchUtilities の型安全性も完全に維持されます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
dab05d97

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)」という模範的な3部構成が明確に適用されています。各セクションの役割も適切です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(`typescript:packages/tailwindcss/src/compile.ts`)やGitHubのIssue/PRリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Tailwind CSSの内部実装に関するトピックを、専門用語を適切に用いて解説しており、専門知識を持つエンジニアという対象読者に完全に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成で、段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落長も適切で非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

`packages/tailwindcss/src/compile.ts`のコード変更点や、テストコードの抜粋が、提供されたDiffと正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`@utility`, `matchUtilities`, `handler`, `null`のセマンティクスなど、PRで使われている技術用語を正確かつ文脈に沿って使用できています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

`utility.options?.types`の有無による条件分岐のロジックや、それが後方互換性を維持する上で果たす役割について、技術的に正確かつ明快に説明されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(問題の背景、解決策、設計判断)がPRのDescriptionやDiffの内容によって裏付けられており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#19777)とIssue番号(#16948)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRのタイトル「Allow multiple @utility definitions with same name but different value types」の内容を的確に日本語で表現しており、主題と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はPRで提供された情報に限定されており、バージョン情報やリリース予定など、PR外の知識を持ち込んでいません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜でした」「〜が修正されました」といった時間表現が、問題の背景と解決策を説明する文脈で適切に使用されています。