2026年9月13日
2026年9月13日
WordPressのブロック開発をTypeScriptで型安全に進める実装ガイド
WordPressのカスタムブロックは、公式のビルドツール @wordpress/scripts をそのまま使えば TypeScript(.ts / .tsx)で書けます。ただし、ビルド時には型が取り除かれるだけで型チェックは行われないため、tsc --noEmit による型チェックを別に用意し、属性(attributes)の型をひとつの場所で定義するのが型安全に進めるための要点です。この記事では、JavaScript版の雛形からTypeScriptへ移す手順、tsconfig.json の設定、属性・編集画面・保存処理の型付け、よくある型エラーの直し方までを順に説明します。
前提として、Node.js と npm が使えるローカル環境と、ブロックを試せるWordPress(ローカル環境やステージング)を用意してください。ブロックはテーマではなくプラグインとして作ると、テーマを切り替えても投稿内のブロックが壊れません。
TypeScript対応の仕組みを先に理解する
@wordpress/scripts(コマンド名は wp-scripts)は内部で webpack と Babel を使っています。既定の Babel 設定(@wordpress/babel-preset-default)には TypeScript の構文を扱うプリセットが含まれているため、.tsx ファイルをエントリーにしてもビルドは通ります。
ここでつまずきやすいのが、「ビルドが通る=型が正しい」ではない点です。Babel は型注釈を削除して JavaScript に変換するだけで、型の矛盾を検出しません。属性名を打ち間違えても、string を期待する場所に number を渡しても、ビルドは成功します。型安全にしたいなら、TypeScript コンパイラ(tsc)を型チェック専用に走らせる工程が欠かせません。
役割分担は次のとおりです。
wp-scripts build/wp-scripts start:変換とバンドル(型は見ない)tsc --noEmit:型チェックだけ(ファイルは出力しない)
この2つを npm スクリプトに並べておき、コミット前やCIで型チェックを実行する運用にします。
手順1:雛形を作ってTypeScriptを追加する
公式の雛形生成ツールでブロックプラグインを作ります。
npx @wordpress/create-block@latest my-notice
cd my-notice
npm install --save-dev typescript
生成直後の src/ には index.js、edit.js、save.js、block.json などが入っています。JavaScriptのファイルを index.ts、edit.tsx、save.tsx に名前を変えます。JSXを含むファイルは .tsx、含まないファイルは .ts にします。
block.json の editorScript は "file:./index.js" のままで構いません。wp-scripts は block.json を読んでエントリーを探す際に、同じ名前の .ts / .tsx も候補に含めます。ビルド後の build/ には index.js が出力されるので、PHP側の参照も変わりません。もし手元のバージョンでエントリーが見つからないと表示された場合は、@wordpress/scripts を更新するか、--webpack-src-dir などのオプションとドキュメントを確認してください(挙動はバージョンにより異なることがあります)。
PHP側は、雛形が生成したとおり init フックで build ディレクトリを登録します。
<?php
/**
* Plugin Name: My Notice Block
*/
defined( 'ABSPATH' ) || exit;
add_action( 'init', function () {
register_block_type( __DIR__ . '/build' );
} );
手順2:tsconfig.json を用意する
プラグインのルートに tsconfig.json を置きます。型チェック専用なので noEmit を有効にします。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": [ "src" ]
}
ポイントは3つです。strict は最初から有効にしておくと、後から有効にするより修正量が少なく済みます。resolveJsonModule は block.json を import するために必要です。skipLibCheck は依存パッケージ側の型定義同士の食い違いでエラーが出るのを避けるためのもので、自分のコードのチェックには影響しません。
package.json の scripts に型チェックを追加します。
{
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"typecheck": "tsc --noEmit"
}
}
手順3:型定義パッケージをそろえる
@wordpress/* のパッケージは、型定義を同梱しているものと、DefinitelyTyped の @types/wordpress__* を追加で入れる必要があるものが混在しています(2026年10月現在)。どれが同梱済みかはパッケージの更新で変わるため、node_modules/@wordpress/パッケージ名/package.json に types の指定があるかを確認し、無いものだけ @types を追加するのが無理のない進め方です。
# 例:型定義が同梱されていないパッケージだけ追加する
npm install --save-dev @types/wordpress__blocks @types/wordpress__block-editor
同梱の型と @types 版を両方入れると、同じ名前の型が二重に定義されて紛らわしいエラーになることがあります。npm run typecheck の結果を見ながら、片方に寄せてください。
手順4:属性の型をひとつの場所で定義する
ブロックの属性は block.json に書きますが、JSON から TypeScript の型は自動では作られません。そこで属性の型を types.ts に定義し、編集画面・保存処理・登録処理のすべてでこの型を使います。
// src/types.ts
export const TONES = [ 'info', 'warning' ] as const;
export type Tone = ( typeof TONES )[ number ];
export interface NoticeAttributes {
message: string;
tone: Tone;
}
export function isTone( value: string ): value is Tone {
return ( TONES as readonly string[] ).includes( value );
}
対応する block.json の属性は次のとおりです。enum と default を書いておくと、エディター側でも想定外の値が入りにくくなります。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/notice",
"title": "お知らせボックス",
"category": "text",
"textdomain": "my-notice",
"attributes": {
"message": { "type": "string", "source": "html", "selector": "p" },
"tone": { "type": "string", "enum": [ "info", "warning" ], "default": "info" }
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
}
注意したいのは、types.ts と block.json の整合性は TypeScript でも検出できないことです。属性を追加・変更したら両方を同時に直す、とチームのルールにしておきます。TONES のように値の一覧を TypeScript 側の定数にしておくと、少なくとも編集画面のコードと選択肢の食い違いは型チェックで見つけられます。
手順5:編集画面と保存処理を型付けする
編集コンポーネントは BlockEditProps に属性の型を渡して受け取ります。こうすると attributes.message は string、setAttributes には NoticeAttributes のキーだけが渡せるようになり、属性名の打ち間違いが型エラーになります。
// src/edit.tsx
import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
import type { BlockEditProps } from '@wordpress/blocks';
import { isTone, type NoticeAttributes } from './types';
export default function Edit( { attributes, setAttributes }: BlockEditProps< NoticeAttributes > ) {
const { message, tone } = attributes;
const blockProps = useBlockProps( { className: `is-tone-${ tone }` } );
return (
<>
<InspectorControls>
<PanelBody title={ __( '表示設定', 'my-notice' ) }>
<SelectControl
label={ __( '種類', 'my-notice' ) }
value={ tone }
options={ [
{ label: __( 'お知らせ', 'my-notice' ), value: 'info' },
{ label: __( '注意', 'my-notice' ), value: 'warning' },
] }
onChange={ ( value: string ) => {
if ( isTone( value ) ) {
setAttributes( { tone: value } );
}
} }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<RichText
tagName="p"
value={ message }
onChange={ ( value: string ) => setAttributes( { message: value } ) }
placeholder={ __( 'お知らせを入力', 'my-notice' ) }
/>
</div>
</>
);
}
SelectControl の onChange が受け取る値は string 型です。ここで value as Tone と型アサーションを書けばエラーは消えますが、それでは型チェックをすり抜けるだけです。上の例のように型ガード関数 isTone() で絞り込むと、想定外の値が入る経路を実際に塞げます。as は「型チェックを止める指示」だと考え、使う場所を最小限にとどめるのが型安全の基本です。
保存処理も同じ型を使います。RichText.Content は保存時のHTMLを出力し、エスケープ処理もブロックエディター側で行われます。
// src/save.tsx
import { useBlockProps, RichText } from '@wordpress/block-editor';
import type { BlockSaveProps } from '@wordpress/blocks';
import type { NoticeAttributes } from './types';
export default function save( { attributes }: BlockSaveProps< NoticeAttributes > ) {
const blockProps = useBlockProps.save( { className: `is-tone-${ attributes.tone }` } );
return (
<div { ...blockProps }>
<RichText.Content tagName="p" value={ attributes.message } />
</div>
);
}
保存処理の出力を後から変えると、既存の投稿で「このブロックには、想定されていないか無効なコンテンツが含まれています」といった検証エラーが出ます(表記はバージョンにより異なります)。型を変えるときも出力HTMLを変えるときも、公開済みのブロックがある場合は deprecated で旧形式を残す必要がある点に注意してください。
手順6:登録処理と型の境界を整理する
最後に index.ts でブロックを登録します。
// src/index.ts
import { registerBlockType, type BlockConfiguration } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
import type { NoticeAttributes } from './types';
import './style.scss';
// block.json から推論される型と BlockConfiguration の型は一致しないことがあるため、
// 型の変換はこの1か所に限定する
registerBlockType< NoticeAttributes >(
metadata as unknown as BlockConfiguration< NoticeAttributes >,
{ edit: Edit, save }
);
import metadata from './block.json' で得られる型は、"type": "string" が文字列リテラルではなく string として推論されるなどの理由で、registerBlockType() が期待する型と合わないことがあります。ここだけは as unknown as で変換を明示し、その代わり編集画面や保存処理では変換を使わない、という線引きにすると、型の穴がどこにあるかが一目で分かります。registerBlockType() の型シグネチャは型定義のバージョンによって細部が異なるため、エラーが出た場合は手元の型定義ファイルを開いて確認してください。
よくある型エラーと直し方
「モジュール '@wordpress/○○' の宣言ファイルが見つかりません」そのパッケージが型定義を同梱していません。対応する @types/wordpress__○○ を追加します。存在しない場合は src/types/wordpress.d.ts に declare module '@wordpress/○○'; と書けば当面のエラーは消えますが、そのモジュールは any 扱いになるので、範囲を限定して使ってください。
**select( 'core/block-editor' ) の戻り値が any になる**ストア名を文字列で渡すと型が付かない場合があります。各パッケージが公開しているストアオブジェクト(例:import { store as blockEditorStore } from '@wordpress/block-editor')を useSelect の中で使うと、型定義があるストアではセレクターの型が効きます。
**ビルドは通るのに npm run typecheck でエラーが大量に出る**これは正常な状態で、Babel が見逃していた問題が表に出ています。一度に直しきれない場合は、ファイル単位で .js のまま残しておき、TypeScriptへ移すファイルを少しずつ増やす進め方もできます(allowJs を有効にすると混在させられます)。
ESLint で TypeScript の構文が解析されないwp-scripts lint-js は、TypeScript がインストールされている場合に TypeScript 用の設定を使うようになっています(細部はバージョンにより異なります)。うまく動かないときは @wordpress/eslint-plugin と @wordpress/scripts のバージョンと設定を確認してください。
運用で気をつけること
@wordpress/scriptsや型定義パッケージを更新すると、型エラーの出方が変わることがあります。更新は単独のコミットにして、npm run buildとnpm run typecheckの両方を確認します。- ビルド成果物の
build/は配布用です。プラグインを本番へ反映する前に、ステージング環境で既存の投稿を開き、ブロックの検証エラーが出ないかを確かめてください。 - 型チェックは CI(GitHub Actions など)で
npm ci && npm run typecheck && npm run buildのように実行しておくと、手元での実行忘れを防げます。
まとめ
@wordpress/scriptsは.ts/.tsxをビルドできますが、Babel は型を削除するだけなので、tsc --noEmitによる型チェックを別に用意します。- 属性の型は
types.tsにまとめ、BlockEditPropsとBlockSavePropsに渡して編集・保存の両方で使います。block.jsonとの整合性は手作業で保つ必要があります。 asによる型の変換は登録処理などの境界に限定し、値の絞り込みには型ガード関数を使います。- 型定義は同梱パッケージと
@types/wordpress__*が混在しているため、手元のバージョンで確認しながら追加します。 - 保存処理の出力変更は既存投稿の検証エラーにつながるので、ステージングで確認してから本番に反映します。