この記事はエムスリー Advent Calendar 2025 9日目の記事です。
デジスマチームの小島(@jiko_21)です。

今回は自分が趣味で実装しているhtml-typeというプロジェクトにて行っている、HTMLタグの持つルールをTypeScriptの型として表現する方法について紹介します。
html-typeについて
html-typeは、TypeScriptの型を用いてHTMLを記述できるライブラリです。
具体的には、次のようにhtml-typeが提供する型を用いて型定義を記述することでHTMLを生成できるようになっています。
type ComplexHtml = Html< Body< [ Div<[P<'First paragraph'>, P<'Second paragraph'>]>, P<'test'>, Div<[P<'test1'>, Div<P<'test2'>>]>, ] > >; // 下記のようなHTMLが生成される // <html> // <body> // <div> // <p>First paragraph</p> // <p>Second paragraph</p> // </div> // <p>test</p> // <div> // <p>test1</p> // <div> // <p>test2</p> // </div> // </div> // </body> // </html>
上記具体例はシンプルなHTMLを表現していますが、<a>タグや<img>タグも実装しており、html-typeの紹介ページもhtml-typeを用いて生成されています。
Html、Bodyなどのタグを表す型はhtml-typeが提供しており、TypeScript Compiler APIを利用してTypeScriptが実際に扱う型のAST(抽象構文木)を解析し、HTMLを生成しています。
TypeScript Compiler APIを用いた型解析については今回省略しますが、TSKaigi Hokuriku 2025にて発表しておりますので下記を参照いただけると幸いです。
ここでは、発表しきれなかったHTMLタグの表現について触れられればと思います。
どのようにHTMLタグを型として表現しているか
そもそもどうしてHTMLタグを型として表現したいのか
HTMLのタグにもセマンティクス(意味論)が存在しており、タグの使い方について詳細なルールが存在します。
例えば、、<span>タグの子要素として<div>タグを記述できません。
<span> インライン要素の中に<div>ブロック要素</div>を置いてはいけない </span>
また、<p>タグの子要素に<div>タグなどブロック要素が記述されると、ブロック要素の手前で<p>タグが閉じられてしまいます。*1
<!---次のようなHTMLは--> <p> <div>ブロック要素</div> </p> <!---ブラウザにはこのように解釈される--> <p></p> <div>ブロック要素</div> <p></p>
このようなセマンティクスのルールについてはHTML解体新書-仕様から紐解く本格入門などの本でも紹介されています。
通常、セマンティクスのチェックをブラウザは行わないため、
- 型としてこれらのルールを表現することでセマンティクス違反を事前に防ぎたい
- セマンティクス違反を何らかの型エラーとして表現したい
といったことがHTMLタグを型で表現したい理由です。
HTMLタグとしてのセマンティクス違反を表現
まずは型でHTMLのタグそのものを表現してみる
いきなり<p>タグや<div>タグを表現するのは難しいので、HTMLのタグが持つ特徴を型で表現していきます。
なお、簡略化のため、classやstyleといったattributeについては今回省略します。*2
まず、HTMLの要素(Element)は子要素として次のようなものを取ります。*3
- HTMLの要素
- 文字列
- HTMLの要素や文字列などのタプル(配列)
HTMLの要素をHTMLElement、そして文字列についてはTextという型で表現すると上記ルールはこのように型で表現できます。
type Text = string; type HTMLElement = | Text | { children: (HTMLElement | Text)[] | HTMLElement | Text; };
これによりシンプルなDOM構造は表現できます。
続いて、<div>タグを表現します。
type Div<T extends HTMLElement[] | HTMLElement> = { children: T; };
TというGenericsを用いて子要素の型を代入できるようにしています。
T extends HTMLElement[] | HTMLElementと指定しているので、TはHTMLElement[]またはHTMLElementを継承したものでなければならず、仮にそれ以外の型、たとえば数字が入っている場合は型エラーとなります。
同様に<p>タグも表現してみます。
type P<T extends HTMLElement[] | HTMLElement> = { children: T; };
これで基本的なタグは定義できました。
<p>タグ内部の<div>タグを制限する
前述の型定義でDOM構造自体は表現できますがセマンティクスによる制限は表現されていません。
ここで、子要素に対して型レベルでの制限を課す必要がでてきますが、TypeScriptには型による二項演算が行えるのでそれを利用します。
具体的にはPの子要素TがDivであるならばnever、そうでないならば子要素Tをもつ型、とします。
type P<T extends HTMLElement[] | HTMLElement> = T extends Div<T extends HTMLElement[] | HTMLElement> ? never : { children: T; };
実際に上記コードを試したのが次のPlaygroundになります。これにより、セマンティクス違反しているケースでは型がneverとなります。 www.typescriptlang.org
セマンティクス違反をわかりやすくする
先程、セマンティクス違反している場合はneverとなるようにしましたが、これでは開発時にエラーがわかりにくくなります。
そこで、neverとなるケースには特別な型を割り当て、IDE上で簡単にわかるようにしてみましょう。
type InvalidPContent<T> = { __error: `❌ <p> cannot contain block elements. Only inline elements are allowed in <p>.`; __invalidType: T; }; type P<T extends HTMLElement[] | HTMLElement> = T extends Div<HTMLElement[] | HTMLElement> ? InvalidPContent<T> : { children: T; };
これにより、いままでneverとなっていた型に対してより具体的なエラー型として表現されるため、よりわかりやすくなりました。

最後に
あまりプロダクションではneverを用いてGenericsに持つ要素を制限する機会が個人的には少なかったですが、子要素を制限する、というルールに用いることができるTypeScriptの型の表現は強力ですね。
これ以外にも、四則演算を型レベルで表現できたりと、型で表現できるものはたくさんあるのでこの機会にぜひ型で実態ある何かを表現してみるのはどうでしょうか?
We are Hiring!
エムスリーでは、フロントエンド・バックエンドに関わらず、新しい技術に興味のあるエンジニアを募集しています。新卒もお待ちしております!