JSX と TSX
Note
JSX/TSX はコンパイラ、リンター、型チェッカー、LSP、フォーマッタで扱えます。 Vue JSX の型チェックはプロジェクトの既定値で有効です。
vite.config.*のvue.typeChecker.jsxTypecheckで変更できます。 React が混在するプロジェクトでは、Vue JSX の型チェックを無効にしてください。 スタンドアロンの.jsx/.tsxモジュールでは、HMR 対応が主な未完了項目です。
Vize は .jsx/.tsx の Vue コンポーネントを、.vue ファイルと同じコンパイラクレートでコンパイルします。
VDOM と Vapor のコード生成、意味解析、型チェック、lint、言語サーバーを利用できます。
VDOM のモジュールでは、元の import、宣言、export、パラメータ、コンポーネント内のレキシカルな束縛を保持します。
JSX/TSX の有効化
Vize のバンドラープラグインは .jsx と .tsx を自動的に処理します。コンパイル用の有効化フラグは不要です。
次のいずれかを導入済みなら、JSX/TSX のコンパイルも利用できます。
@vizejs/vite-plugin@vizejs/unplugin(Rollup / webpack / esbuild)@vizejs/rspack-plugin@vizejs/nuxt
// vite.config.ts — nothing JSX-specific is required
import { defineConfig } from "vite";
import vize from "@vizejs/vite-plugin";
export default defineConfig({
plugins: [vize()],
});
プラグインは @vizejs/native または @vizejs/wasm の compileJsx を呼び出します。
この処理でソースを中間表現へ変換し、レンダー関数のコードと、抽出した scoped CSS を返します。
コンポーネントの書き方
Vize の JSX/TSX コンポーネントは、型付きパラメータを持つ通常の関数です。
一般的なケースではマクロや defineComponent によるラップは不要です。
関数シグネチャから型を読み取り、実行時の出力から型注釈を消去するため、型のための実行時コストは発生しません。
- props は、型を付けた第 1 引数です。
- emits と slots は、第 2 引数の
Ctx<Emits, Slots>で表します。Vize が提供するこのコンテキストには、Vue の setup コンテキストと同様にemit、slots、attrsがあります。 - props のデフォルト値は、引数の分割代入に指定します。コンパイラがその値を取り出します。
import { computed, ref } from "vue";
type CounterProps = {
label: string;
start?: number;
};
type CounterEmits = {
change: [value: number];
};
const Counter = ({ label, start = 0 }: CounterProps, { emit }: Ctx<CounterEmits>) => {
const count = ref(start);
const doubled = computed(() => count.value * 2);
const increment = () => {
count.value += 1;
emit("change", count.value);
};
return (
<section class="counter">
<p>
{label}: {count.value}
</p>
<p>Double: {doubled.value}</p>
<button type="button" onClick={increment}>
Increment
</button>
</section>
);
};
props だけを受け取るコンポーネントでは、第 2 引数を省略できます。
const Hello = ({ name }: { name: string }) => <h1>Hello, {name}!</h1>;
デフォルト値も分割代入で指定できるため、別途 props オプションを書く必要はありません。
const Badge = ({ count = 0 }: { count?: number }) => <span class="badge">{count}</span>;
コンポーネント名は、変数の束縛(const Counter = …)または関数宣言(function Card() { … })から取得します。
要素の入れ子、フラグメント(<>…</>)、子要素内の式、onClick などのイベント用 props は、React と同様の JSX 構文です。
Vue 固有の追加として、後述の <style scoped> があります。
VDOM のブロック本体を持つコンポーネントでは、分割代入またはインラインのオブジェクト型から実行時の props 名を宣言します。 実行時バリデータ、名前付きの型からの推論、明示的な
defineComponentの setup 形式は未対応です。 ブロック内の setup 処理でレキシカルなthis、arguments、new.targetを使うと診断します。
対応する JSX 構文
JSX は SFC テンプレートと同じ中間表現に変換され、VDOM または Vapor のバックエンドへ渡されます。 JSX/TSX のテストで確認している構文は次のとおりです。
- フラグメントと要素の入れ子
- コンポーネントタグ、メンバー式によるタグ、組み込みの HTML/SVG タグ
- 静的属性、動的な
prop={expr}、値を省略した真偽値 props、スプレッド props - イベントハンドラー。Vue のイベントオプション修飾子は props 名で指定できます。
v-if、v-else-if、v-else、v-show、独自のv-*ディレクティブ、v-model- 子要素内の式、論理演算子や三項演算子による分岐、
.map(...)によるリスト描画 - 子要素のオブジェクトや render prop で記述したスロット
- 型付き引数、戻り値の型注釈、ジェネリックな JSX 呼び出し、キャスト、非 null アサーション
<style scoped>の抽出。テンプレートリテラルの${expr}も扱えますが、通常は静的クラスと CSS 変数の方が読みやすくなります。
リストは、通常の JSX と同様に .map(...) で記述します。
import { computed, ref } from "vue";
type Todo = {
id: string;
title: string;
done: boolean;
};
type TodoListProps = {
todos: Todo[];
initialActiveId?: string;
};
const TodoList = ({ todos, initialActiveId }: TodoListProps) => {
const activeId = ref(initialActiveId ?? todos[0]?.id);
const activeTodo = computed(() => todos.find((todo) => todo.id === activeId.value));
return (
<section class="todo-panel">
<header>
<h2>{activeTodo.value?.title ?? "Select a todo"}</h2>
</header>
<ul class="todo-list">
{todos.map((todo, index) => (
<li
key={todo.id}
class={{ done: todo.done, active: todo.id === activeId.value }}
data-index={index}
>
<button type="button" onClick={() => (activeId.value = todo.id)}>
<span>{todo.title}</span>
{todo.id === activeId.value ? <strong>Active</strong> : <em>{index + 1}</em>}
</button>
</li>
))}
</ul>
</section>
);
};
.map(...) のコールバック引数(todo、index)は、型チェッカーと LSP 用の仮想 TypeScript でもスコープ内に保持します。
ホバー、補完、診断、名前変更は、元のソースにある同じ束縛を対象にします。
出力モード: VDOM と Vapor
コンポーネントは、Vue の標準レンダラーである Virtual DOM または Vapor 向けにコンパイルされます。 設定でデフォルトを選び、コンポーネントごとに上書きできます。
デフォルト設定
compiler.jsxMode は .jsx/.tsx 全体のデフォルトを指定します。
値は "vdom" または "vapor" で、デフォルトは "vdom" です。
// vite.config.ts
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
compiler: {
// Default every .jsx/.tsx component to Vapor output.
jsxMode: "vapor",
},
},
});
jsxMode と compiler.vapor は独立しています。vapor は .vue SFC を、jsxMode は JSX/TSX を対象にします。
SFC を VDOM、JSX を Vapor にする構成も、その逆も可能です。
Vite プラグインに直接指定した jsxMode は、共有設定より優先されます。
コンポーネントごとのディレクティブ
"use strict" と同様に、関数の先頭へディレクティブを書いてデフォルトを上書きします。
// Compiled to Vapor regardless of the configured default.
const Fast = () => {
"use vue:vapor";
return <div class="fast" />;
};
// Compiled to Virtual DOM regardless of the configured default.
const Classic = () => {
"use vue:vdom";
return <div class="classic" />;
};
各コンポーネントの出力先は独立しているため、1 つのファイルで VDOM と Vapor を併用できます。
// vite.config.ts: { vue: { compiler: { jsxMode: "vapor" } } }
// No directive -> takes the configured default (Vapor here).
export const Dashboard = () => <main>{/* ... */}</main>;
// Opts back into Virtual DOM just for this component.
export const LegacyWidget = () => {
"use vue:vdom";
return <aside>{/* ... */}</aside>;
};
優先順位
出力モードは次の順で決まります。
- コンポーネント先頭の
"use vue:vapor"/"use vue:vdom"。 - 共有設定の
compiler.jsxMode、またはプラグインオプションのjsxMode。 - 組み込みのデフォルト
"vdom"。
診断
無効なディレクティブや指定の衝突は診断します。
"use vue:"で始まり、既知のモードを指定していないものはコンパイルエラーです。例:"use vue:vdomx"。- 1 つのコンポーネントで
"use vue:vapor"と"use vue:vdom"を指定すると診断します。出力モードには先に書いた指定が使われます。 "use strict"など、モードに関係しないディレクティブは保持します。
スコープ付きスタイル
コンポーネント内の <style scoped> は、SFC の同名のブロックに相当します。
コンパイル時に抽出するため、実行時の <style> vnode としては描画しません。
生成した data-v-<hash> スコープ ID で CSS を書き換え、他の要素にスコープ属性を付け、バンドラープラグインの CSS 処理へ渡します。
VDOM と Vapor の両方で利用でき、同じコンポーネントには同じスコープ ID が付きます。
通常は SFC の <template> → <style> と同じ順で、マークアップの末尾へ配置します。
ただし、コンパイラは記述位置にかかわらず抽出します。
type CardProps = {
title: string;
};
const Card = ({ title }: CardProps) => (
<article class="card">
<h2>{title}</h2>
<style scoped>{`
.card {
border: 1px solid currentColor;
padding: 12px;
}
`}</style>
</article>
);
動的スタイル値
動的な値には、クラスの束縛、インラインのスタイルオブジェクト、CSS カスタムプロパティを使うと読みやすくなります。
<style scoped> 内の ${expr} によるテンプレートリテラル補間も型チェックできますが、必要な場合に使う補助手段です。
type BoxProps = {
color: string;
gap: number;
};
const Box = ({ color, gap }: BoxProps) => (
<section
class="box"
style={{
"--box-color": color,
"--box-gap": `${gap}px`,
}}
>
<p>content</p>
<style scoped>{`
.box {
color: var(--box-color);
gap: var(--box-gap);
}
`}</style>
</section>
);
scoped を付けていない <style> は通常の要素として描画され、抽出されません。
<style scoped>{`.box { color: ${color}; }`}</style> も型チェックの対象です。
scoped スタイルシートからコンポーネント内の式を参照する必要がある場合に使ってください。
SFC の <style> で使う CSS の v-bind(...) 構文は、JSX のスタイルブロックでは未対応です。
フォーマット
Glyph は OXC のパーサーとフォーマッタで JSX/TSX を整形します。
.vue 内の <script lang="jsx">、<script lang="tsx">、<script setup lang="tsx"> も JSX/TSX として解析するため、
JSX の子要素や TSX の型注釈を、通常の TypeScript と区別して整形します。
<script setup lang="tsx">
type CardProps = {
title: string;
items: string[];
};
const Card = ({ title, items }: CardProps) => (
<section class="card">
<h2>{title}</h2>
{items.map((item) => (
<span key={item}>{item}</span>
))}
</section>
);
</script>
vize fmt は .vue に加えてスタンドアロンの .jsx/.tsx も検出し、同じ JSX/TSX の処理で整形します。
# Formats .vue, .jsx, and .tsx files by default
vize fmt src --write
型チェック
Vue JSX の型チェックはプロジェクトの既定値で有効です。vite.config.* の vue.typeChecker.jsxTypecheck で変更できます。
Vue と React が混在する場合は、jsxTypecheck: false にして React の .tsx を TypeScript に任せてください。Vue SFC の型チェックは引き続き有効です。
// vite.config.ts
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
typeChecker: {
enabled: true,
jsxTypecheck: true,
},
},
});
有効にすると、vize check は .jsx/.tsx の Vue コンポーネントを型チェックします。
生成する仮想ファイルは TSX ではなく通常の TypeScript で、次のように元のコンポーネントの型情報を保持します。
- 第 1 引数の型を props の型として保持します。
Ctx<Emits, Slots>を setup 本体と JSX の式から参照できます。- イベントハンドラー、props の束縛、
v-if/v-show、独自ディレクティブ、scoped スタイル内の補間式は、TypeScript の読み取り式として出力します。 v-modelの代入先は書き込みの検査用に自己代入として出力し、readonly な値や左辺に置けない式を元の束縛位置で診断します。.map(...)の本体は生成したコールバック内へ出力し、値とインデックスの引数には推論した要素型を保持します。
仮想 TypeScript の意味のある範囲を元のソース範囲へ対応付けるため、CLI の JSON と LSP の診断はいずれも元のソース位置を指します。
type FieldProps = {
model: {
readonly value: string;
};
};
const Field = ({ model }: FieldProps) => <input v-model={model.value} />;
この例では model.value を代入先として検査します。readonly な場合は、生成コードではなく TSX ソース内の model.value を診断します。
# Type-check a project including its .jsx/.tsx Vue components.
# .jsx/.tsx files are collected only when typeChecker.jsxTypecheck is enabled.
vize check src
スタンドアロンの JSX/TSX は、型チェック用の通常の TypeScript へ変換します。
一方、SFC の <script lang="jsx">、<script lang="tsx">、対応する script setup は .vue.tsx の仮想ファイルにします。
これにより、TypeScript はスクリプトブロック内の JSX を解析できます。
LSP と CLI は同じ変換を使うため、TypeScript の診断はエディターでもコマンドラインでも同じソース範囲を指します。
エディター / LSP
vize lsp に対応したエディターでは、.jsx/.tsx の Vue コンポーネントに SFC と同じ機能を使えます。
SFC でラップする必要はありません。
- 診断
- ホバー
- 補完
- 定義への移動
- 参照の検索
- 名前変更
- ドキュメントシンボル
- セマンティックトークン
- コードアクション
<style scoped>内の CSS 診断
ドキュメントシンボル、セマンティックトークン、scoped スタイルの診断、コードアクションは、解析した文書から利用できます。
型情報を使う診断、ホバー、補完、定義への移動、参照、名前変更には typeChecker.jsxTypecheck の有効化が必要です。
React が混在するプロジェクトでは、エディターでも Vue JSX の型チェックを明示的に無効にしてください。
lint
Vize の lint ルールは JSX/TSX を直接検査します。
マークアップ用ルールは疑似的な SFC テンプレートを作らず、JSX の要素と属性を直接読み取ります。
.map(...) の key 検査など、Vue テンプレートの形が必要なルールは変換後のテンプレートのツリーを使います。
セマンティックなルールは、SFC と同じ解析を利用します。
そのため、文字列の部分一致に頼らず、JSX/TSX の構文から次の問題を検出できます。
const BrokenMedia = () => (
<article>
<img src="/avatar.png" />
<button accessKey="s" autoFocus>
Save
</button>
</article>
);
上の例では、次のルールが JSX のソースを診断します。
a11y/img-alt:altの指定漏れ。a11y/no-access-key:accessKeyの使用。a11y/no-autofocus:autoFocusの使用。
リストの key を調べるルールも、通常の .map(...) による記述を扱えます。
const KeyedList = ({ rows }: { rows: Array<{ id: string; label: string }> }) => (
<ul>
{rows.map((row) => (
<li key={row.id}>{row.label}</li>
))}
</ul>
);
診断と修正は JSX のソース範囲へ対応付けられます。CLI の出力とエディターの表示は、修正する要素や props を指します。
# Lint .vue, .html, .jsx, and .tsx files
vize lint src
lint と型チェックの仕組みは静的解析を、具体的なルール出力はルール一覧を参照してください。
制限事項
現在の制限は次のとおりです。
- Vue と React が混在するプロジェクトでは無効にする必要があります。
vue.typeChecker.jsxTypecheck: falseを設定し、React の.tsxを Vue コンポーネントとして検査しないようにしてください。 .jsx/.tsxモジュールの HMR は未対応です。 VDOM は元の import、export、ブロック内の setup 状態、JSX の引数のデフォルト値とスコープを保持します。ただし、バンドラーが Vue の HMR 境界を登録しないため、編集時は通常の再読み込みになります。- Vapor と SSR で元のモジュールを保持する処理は未完了です。 スタンドアロンのレンダラーが保持できない import、export、setup 文、捕捉したローカルの束縛は診断します。静的なスタンドアロンのレンダラーとコンポーネント単位の出力は利用できます。
- 実行時ヘルパーの別名は隠せません。
_openBlockなどの生成するヘルパーと元の束縛が衝突すると、不正なモジュールを出力せず診断します。 - JSX の
<style scoped>内では CSS のv-bind(...)は未対応です。 型チェックできる${expr}によるテンプレートリテラル補間を使ってください。
関連項目
- 設定:
compiler.jsxMode、typeChecker.jsxTypecheck、共有設定全体。 - Vite プラグイン: 推奨するバンドラー連携。
- 静的解析: lint と型チェックで共有するコンパイラの処理。
examples/jsx-tsx: コンパイラ、リンター、型チェッカー、LSP、フォーマッタの確認用 JSX/TSX 例。