- TypeScript
TypeScript デコレーターの使い方|標準と旧方式の違いと書き方
TypeScript のデコレーターとは、クラスやそのメンバーの直前に「@名前」と書いて、ログ出力や入力チェックなどの処理を後付けする関数です。 書き方は2種類あり、TypeScript 5.0 以降で設定なしに使える標準方式(引数は value と context の2つ)と、tsconfig で experimentalDecorators を有効にして使う旧方式(引数は target・キー・descriptor)では、関数の形がまったく違います。
この記事では、同じログ出力デコレーターを両方式で書き、tsc が出力する JavaScript と node の実行結果を並べて違いを確かめます。そのうえで、標準方式の5種類(クラス・メソッド・getter/setter・フィールド・accessor)の引数と戻り値、コピペできる最小コード、重ねたときの評価順、方式を取り違えたときのエラー全文までを順に載せます。
確認環境:Ubuntu 24.04 / TypeScript 7.0.2(npx tsc --version の表示は Version 7.0.2)/ Node.js 22.22.2 と 24.21.0。掲載した出力はすべてこの環境で実行したものです。
デコレーターの早見表(標準方式・5種類)
標準方式のデコレーターは、どの種類でも (value, context) の2引数を受け取ります。違うのは、第1引数の中身と、返せる値です。
| 付ける場所 | 第1引数 value | context.kind | 返せる値(返さなくてもよい) | context の型名 |
|---|---|---|---|---|
| クラス | クラスそのもの | class | 置き換えるクラス | ClassDecoratorContext |
| メソッド | 元のメソッド関数 | method | 置き換える関数 | ClassMethodDecoratorContext |
| getter | 元の getter 関数 | getter | 置き換える getter 関数 | ClassGetterDecoratorContext |
| setter | 元の setter 関数 | setter | 置き換える setter 関数 | ClassSetterDecoratorContext |
| フィールド | undefined | field | 初期値を受け取り、新しい初期値を返す関数 | ClassFieldDecoratorContext |
| accessor | get と set を持つオブジェクト | accessor | get・set・init のうち必要なものを持つオブジェクト | ClassAccessorDecoratorContext |
context には、kind のほかに name(メンバー名)・static・private・addInitializer・metadata が入っています。addInitializer は「あとで実行する処理」を登録する関数で、bind のように this が必要な処理に使います。
標準方式と旧方式の違いは次のとおりです。
| 比べる点 | 標準方式(TS 5.0以降) | 旧方式(experimentalDecorators) |
|---|---|---|
| 必要な tsconfig | 追加の設定なし | "experimentalDecorators": true |
| 引数の形 | (value, context) の2つ | クラスは (constructor)、メソッドと getter/setter は (target, キー, descriptor)、プロパティは (target, キー) |
| 処理を差し替える方法 | 新しい関数やクラスを return する | descriptor.value を書き換える(descriptor を return してもよい) |
| フィールド(プロパティ) | 初期値を加工する関数を返せる | 戻り値は使われない |
| パラメータデコレーター | 書けない(TS1206) | 書ける(target, キー, 引数の位置) |
| emitDecoratorMetadata | 併用できない(TS5052) | 併用できる(design:type などを出力) |
| accessor キーワードへの付与 | できる | この記事では扱いません |
| メタデータ | context.metadata と Symbol.metadata(TS 5.2以降) | reflect-metadata ライブラリと組み合わせる |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript デコレーターの使い方|標準と旧方式の違いと書き方」
同じログ出力デコレーターを両方式で書いて比べる
メソッドの開始と終了を記録するデコレーターを、両方式で書きます。クラス側の書き方(@log を付ける)は同じで、違うのはデコレーター関数の中身と tsconfig だけです。
標準方式のコードと tsconfig
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"strict": true,
"rootDir": ".",
"outDir": "dist"
}
}
function log<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`[log] ${name} 開始 引数=${JSON.stringify(args)}`);
const result = target.call(this, ...args);
console.log(`[log] ${name} 終了 戻り値=${JSON.stringify(result)}`);
return result;
};
}
class Cart {
@log
total(price: number, count: number): number {
return price * count;
}
}
console.log(new Cart().total(120, 3));
第1引数 target が元のメソッド、第2引数 context がメンバーの情報です。新しい関数を return すると、メソッドがその関数に置き換わります。 型引数の This・Args・Return は、元のメソッドの this・引数・戻り値の型をそのまま引き継ぐためのものです。
旧方式のコードと tsconfig
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"strict": true,
"rootDir": ".",
"outDir": "dist",
"experimentalDecorators": true
}
}
function log(
target: object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
): void {
const original = descriptor.value;
const name = String(propertyKey);
descriptor.value = function (this: unknown, ...args: unknown[]) {
console.log(`[log] ${name} 開始 引数=${JSON.stringify(args)}`);
const result = original.apply(this, args);
console.log(`[log] ${name} 終了 戻り値=${JSON.stringify(result)}`);
return result;
};
}
class Cart {
@log
total(price: number, count: number): number {
return price * count;
}
}
console.log(new Cart().total(120, 3));
旧方式では、第3引数の descriptor.value に元のメソッドが入っているので、それを新しい関数で上書きします。
node の実行結果は同じ
どちらも npx tsc -p . でコンパイルし、node dist/log.js で実行しました。出力は Node.js 22.22.2 と 24.21.0 で同じです。
[log] total 開始 引数=[120,3]
[log] total 終了 戻り値=360
360
tsc が出力する JavaScript は大きく違う
標準方式(target は es2022)の dist/log.js です。補助関数の中身は長いので省略しています。
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
…(省略)…
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
…(省略)…
};
function log(target, context) {
const name = String(context.name);
return function (...args) {
console.log(`[log] ${name} 開始 引数=${JSON.stringify(args)}`);
const result = target.call(this, ...args);
console.log(`[log] ${name} 終了 戻り値=${JSON.stringify(result)}`);
return result;
};
}
let Cart = (() => {
let _instanceExtraInitializers = [];
let _total_decorators;
return class Cart {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
_total_decorators = [log];
__esDecorate(this, null, _total_decorators, { kind: "method", name: "total", static: false, private: false, access: { has: obj => "total" in obj, get: obj => obj.total }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
total(price, count) {
return price * count;
}
constructor() {
__runInitializers(this, _instanceExtraInitializers);
}
};
})();
console.log(new Cart().total(120, 3));
export {};
旧方式の dist/log.js です。
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
…(省略)…
};
function log(target, propertyKey, descriptor) {
const original = descriptor.value;
const name = String(propertyKey);
descriptor.value = function (...args) {
console.log(`[log] ${name} 開始 引数=${JSON.stringify(args)}`);
const result = original.apply(this, args);
console.log(`[log] ${name} 終了 戻り値=${JSON.stringify(result)}`);
return result;
};
}
class Cart {
total(price, count) {
return price * count;
}
}
__decorate([
log
], Cart.prototype, "total", null);
console.log(new Cart().total(120, 3));
export {};
読み取れることは3つです。
- 標準方式は、context オブジェクト(kind・name・static・private・access・metadata)を tsc が組み立てて渡している
- 標準方式は、クラスの static ブロックの中でデコレーターを適用し、addInitializer で登録された処理を constructor で実行する
- 旧方式は、クラス定義のあとで __decorate を呼び、Cart.prototype と "total" を渡している
TypeScript 7.0 と Node.js 22/24 で動かす手順
標準方式のデコレーターを Node.js で動かすときは、tsc で target を es2022 にして変換した JavaScript を実行します。手順は次のとおりです。
mkdir deco-sample && cd deco-sample
npm init -y
npm pkg set type=module
npm i -D typescript
npx tsc --version
# 上の tsconfig.json と log.ts を置く
npx tsc -p .
node dist/log.js
target と lib の設定で結果が変わる点を、実際に試した範囲でまとめます。
| 設定・実行方法 | 結果 |
|---|---|
| target を es2022 にして tsc で変換し、node で実行 | Node.js 22.22.2・24.21.0 とも動く |
| target を esnext にして tsc で変換し、node で実行 | 出力 JS に @log がそのまま残り、どちらの Node.js でも SyntaxError |
| node log.ts と .ts を直接実行 | どちらの Node.js でも SyntaxError |
| lib を指定しない | デコレーターの型(ClassMethodDecoratorContext など)は使える |
| lib を指定せずに Symbol.metadata を使う | TS2550(後述)。lib に esnext.decorators を加える |
target を esnext にしたときの出力と実行結果です。
class Cart {
@log
total(price, count) {
return price * count;
}
}
dist-next/log.js:11
@log
^
SyntaxError: Invalid or unexpected token
@ の構文を Node.js 22・24 はそのままでは解釈しないため、target は es2022 にして tsc に変換を任せるのが、この環境で確かめられた動かし方です。
5種類のデコレーターが受け取る引数を実行して確かめる
早見表の内容を、1つのクラスに5種類を全部付けて確かめます。
function show(label: string) {
return function (value: unknown, context: DecoratorContext) {
const v =
value === undefined ? "undefined"
: typeof value === "function" ? "function"
: `{ ${Object.keys(value as object).join(", ")} }`;
console.log(`${label}: 第1引数=${v} kind=${context.kind} name=${String(context.name)}`);
};
}
@show("クラス")
class Item {
@show("フィールド") title = "ノート";
@show("accessor") accessor stock = 3;
@show("メソッド") rename(t: string) { this.title = t; }
@show("getter") get label() { return this.title; }
@show("setter") set label(v: string) { this.title = v; }
}
accessor: 第1引数={ get, set } kind=accessor name=stock
メソッド: 第1引数=function kind=method name=rename
getter: 第1引数=function kind=getter name=label
setter: 第1引数=function kind=setter name=label
フィールド: 第1引数=undefined kind=field name=title
クラス: 第1引数=function kind=class name=Item
フィールドの第1引数は undefined です。フィールドはインスタンスごとに作られるため、デコレーターが呼ばれる時点(クラス定義時)にはまだ値がありません。値に手を入れたいときは「初期値を受け取る関数」を返します。
accessor は、フィールド宣言の前に付けるキーワードです。accessor stock = 3 と書くと、非公開の保存場所と、それを読み書きする getter・setter が自動で作られます。デコレーターは get と set を受け取れるので、代入のたびに処理をはさみたいときは、フィールドではなく accessor に付けます。
コピペで使える最小コード(標準方式)
ログ出力(メソッド)
前の章の log がそのまま使えます。引数に名前を渡したいときは、デコレーターを返す関数(デコレーターファクトリー)にします。評価順の章の wrap がその形です。
実行時間計測・bind・入力チェック・初期値の加工・クラスの登録
5つのデコレーターを1ファイルにまとめた完成形です。tsconfig は前の章と同じです。
// 1. 実行時間を測る(メソッド)
function timed<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
const start = performance.now();
try {
return target.call(this, ...args);
} finally {
const ms = performance.now() - start;
console.log(`[timed] ${name}: ${ms < 1000 ? "1秒以内" : "1秒超"}`);
}
};
}
// 2. this を固定する(メソッド+addInitializer)
function bound<This extends object, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const name = context.name;
context.addInitializer(function () {
Object.defineProperty(this, name, {
value: target.bind(this),
writable: true,
configurable: true,
});
});
}
// 3. 入力チェック(accessor)
function range(min: number, max: number) {
return function <This>(
target: ClassAccessorDecoratorTarget<This, number>,
context: ClassAccessorDecoratorContext<This, number>
): ClassAccessorDecoratorResult<This, number> {
const name = String(context.name);
const check = (v: number) => {
if (!Number.isInteger(v) || v < min || v > max) {
throw new RangeError(`${name} は ${min}〜${max} の整数にしてください(渡された値: ${v})`);
}
return v;
};
return {
init(value) { return check(value); },
get() { return target.get.call(this); },
set(value) { target.set.call(this, check(value)); },
};
};
}
// 4. フィールドの初期値を加工する(フィールド)
function trimmed<This>(
_target: undefined,
_context: ClassFieldDecoratorContext<This, string>
) {
return function (this: This, initial: string): string {
return initial.trim();
};
}
// 5. クラスを登録する(クラス)
const registry = new Map<string, Function>();
function register(name: string) {
return function <T extends abstract new (...args: any[]) => object>(
target: T,
_context: ClassDecoratorContext<T>
) {
registry.set(name, target);
};
}
@register("item")
class Item {
@trimmed title = " ノート ";
@range(0, 99) accessor stock = 3;
@timed
sum(n: number): number {
let s = 0;
for (let i = 1; i <= n; i++) s += i;
return s;
}
@bound
describe(): string {
return `${this.title} (在庫 ${this.stock})`;
}
}
const item = new Item();
console.log(JSON.stringify(item.title));
console.log(item.sum(1000));
const { describe } = item;
console.log(describe());
item.stock = 10;
console.log(item.stock);
try {
item.stock = 120;
} catch (e) {
console.log(`${(e as Error).name}: ${(e as Error).message}`);
}
console.log([...registry.keys()]);
"ノート"
[timed] sum: 1秒以内
500500
ノート (在庫 3)
10
RangeError: stock は 0〜99 の整数にしてください(渡された値: 120)
[ 'item' ]
それぞれのポイントです。
| デコレーター | 付ける場所 | 使っている仕組み | ポイント |
|---|---|---|---|
| timed | メソッド | 関数を返して置き換え | try と finally で、例外が出ても時間を記録する。ミリ秒をそのまま出すなら ms.toFixed(1) を使う |
| bound | メソッド | context.addInitializer | インスタンス生成時に this を固定した関数を登録する。分割代入で取り出しても this が保たれる |
| range | accessor | get・set・init を返す | init で初期値、set で代入時の値を検査する |
| trimmed | フィールド | 初期値を加工する関数を返す | 代入時には呼ばれない。初期値だけに効く |
| register | クラス | 何も返さず、副作用で登録 | クラスを Map に登録する |
bound を付けないと、const { describe } = item のように取り出したメソッドは this を持たずに呼ばれます。イベントハンドラーやコールバックにメソッドを渡すクラスでは、bound が役に立ちます。
クラスの置き換え・getter の結果の保存・setter の記録
残りの種類の例です。
// クラス:サブクラスを返して置き換える
function withCreatedAt<T extends new (...args: any[]) => object>(
target: T,
_context: ClassDecoratorContext<T>
) {
return class extends target {
createdAt = "2026-10-05";
};
}
// getter:結果を1回だけ計算して覚える
function memo<This extends object, Return>(
target: (this: This) => Return,
_context: ClassGetterDecoratorContext<This, Return>
) {
const cache = new WeakMap<This, Return>();
return function (this: This): Return {
if (!cache.has(this)) cache.set(this, target.call(this));
return cache.get(this) as Return;
};
}
// setter:代入のたびに記録する
function logSet<This, Value>(
target: (this: This, value: Value) => void,
context: ClassSetterDecoratorContext<This, Value>
) {
return function (this: This, value: Value): void {
console.log(`[set] ${String(context.name)} = ${JSON.stringify(value)}`);
target.call(this, value);
};
}
@withCreatedAt
class Report {
#title = "月次";
@memo
get heavy(): number {
console.log("計算中…");
return 42;
}
@logSet
set title(v: string) { this.#title = v; }
get title(): string { return this.#title; }
}
const r = new Report();
console.log(r.heavy, r.heavy);
r.title = "週次";
console.log(r.title, JSON.stringify(r));
計算中…
42 42
[set] title = "週次"
週次 {"createdAt":"2026-10-05"}
getter は2回読んでも「計算中…」が1回だけ出ています。クラスデコレーターがサブクラスを返したので、createdAt が追加されました。
複数重ねたときの評価順
同じメソッドに2つ付けたとき
function wrap(label: string) {
return function <This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
_context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
return function (this: This, ...args: Args): Return {
console.log(`${label} 前`);
const result = target.call(this, ...args);
console.log(`${label} 後`);
return result;
};
};
}
class Task {
@wrap("外側")
@wrap("内側")
run() {
console.log("本体");
}
}
new Task().run();
外側 前
内側 前
本体
内側 後
外側 後
メソッドに近いほう(下に書いたほう)が先に適用され、上に書いたほうがそれを包みます。 呼び出したときは、上に書いたものから順に動きます。
クラス全体での順番(標準方式)
function d(label: string) {
console.log(`式を評価: ${label}`);
return function (value: unknown, context: DecoratorContext) {
console.log(`適用: ${label} (${context.kind} ${String(context.name)})`);
context.addInitializer(function () {
console.log(`initializer: ${label}`);
});
};
}
@d("A")
@d("B")
class Order {
@d("C")
static count = 0;
@d("D")
id = 1;
@d("E")
@d("F")
send() {}
}
console.log("--- new Order() ---");
new Order();
式を評価: A
式を評価: B
式を評価: C
式を評価: D
式を評価: E
式を評価: F
適用: F (method send)
適用: E (method send)
適用: C (field count)
適用: D (field id)
適用: B (class Order)
適用: A (class Order)
initializer: C
initializer: B
initializer: A
--- new Order() ---
initializer: F
initializer: E
initializer: D
この出力から、標準方式の順番は次のようにまとめられます。
- @ の右側の式(d("A") など)を、上から書いた順にすべて評価する
- メソッドのデコレーターを適用する(同じメンバーの中では下から上)
- フィールドのデコレーターを適用する(この例では static、インスタンスの順)
- クラスのデコレーターを適用する(下から上)
- static メンバーとクラスの addInitializer は、クラス定義の最後に実行される
- インスタンスメンバーの addInitializer は、new のたびに実行される
クラス全体での順番(旧方式)
旧方式で同じ構成にパラメータデコレーター G を加えたコードです。旧方式のデコレーターは種類によって引数の数が違うので、可変長引数で受けています。
function d(label: string) {
console.log(`式を評価: ${label}`);
return function (...args: unknown[]) {
const [target, key, third] = args;
const where = typeof target === "function" && key === undefined ? "class" : String(key);
console.log(`適用: ${label} (${where}) 引数の数=${args.length} 第3引数=${typeof third === "object" ? "descriptor" : String(third)}`);
};
}
@d("A")
@d("B")
class Order {
@d("C")
static count = 0;
@d("D")
id = 1;
@d("E")
@d("F")
send(@d("G") to: string) {}
}
式を評価: D
適用: D (id) 引数の数=3 第3引数=undefined
式を評価: E
式を評価: F
式を評価: G
適用: G (send) 引数の数=3 第3引数=0
適用: F (send) 引数の数=3 第3引数=descriptor
適用: E (send) 引数の数=3 第3引数=descriptor
式を評価: C
適用: C (count) 引数の数=3 第3引数=undefined
式を評価: A
式を評価: B
適用: B (class) 引数の数=1 第3引数=undefined
適用: A (class) 引数の数=1 第3引数=undefined
旧方式は「メンバーごとに、式の評価と適用を続けて行う」動きで、インスタンスメンバー、static メンバー、クラスの順に進みます。 標準方式が最初に式をまとめて評価するのとは順番が違うので、デコレーターファクトリーの中で副作用のある処理をしている場合は、方式を変えると実行順が変わります。パラメータデコレーター G の第3引数には、引数の位置を表す 0 が渡されています。プロパティのデコレーター(C と D)の第3引数は undefined でした。
方式を取り違えたときのエラー全文
記事やライブラリのサンプルが、自分の tsconfig と違う方式で書かれていると、次のエラーが出ます。「2 arguments」「3」という数字が出たら、方式の取り違えを疑うと早く直せます。
旧方式の書き方を、標準方式(設定なし)でコンパイルした場合
// 旧方式の書き方(引数3つ)のデコレーター
function log(target: object, key: string, descriptor: PropertyDescriptor): void {
console.log(key);
}
function sealed(constructor: Function): void {
Object.seal(constructor);
}
function required(target: object, key: string | symbol, index: number): void {}
function prop(target: object, key: string): void {}
@sealed
class Cart {
@prop
name = "";
@log
total(): number { return 0; }
add(@required item: string): void {}
}
legacy_in_std.ts(13,4): error TS1240: Unable to resolve signature of property decorator when called as an expression.
Argument of type 'undefined' is not assignable to parameter of type 'object'.
legacy_in_std.ts(16,3): error TS1241: Unable to resolve signature of method decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
legacy_in_std.ts(19,7): error TS1206: Decorators are not valid here.
--locale ja を付けたときの表示です。
legacy_in_std.ts(13,4): error TS1240: 式として呼び出される場合、プロパティ デコレーターのシグネチャを解決できません。
型 'undefined' の引数を型 'object' のパラメーターに割り当てることはできません。
legacy_in_std.ts(16,3): error TS1241: 式として呼び出される場合、メソッド デコレーターのシグネチャを解決できません。
このランタイムは 2 引数を指定してデコレーターを呼び出しますが、デコレーターには 3 が必要です。
legacy_in_std.ts(19,7): error TS1206: デコレーターはここでは無効です。
クラスデコレーターの sealed は引数が1つなので、型の上ではエラーになりませんでした。直し方は、tsconfig に experimentalDecorators: true を入れる(旧方式として使う)か、デコレーターを (value, context) の形に書き直すかのどちらかです。パラメータデコレーターは標準方式にないので、そのライブラリを使うあいだは旧方式を選びます。
標準方式の書き方を、experimentalDecorators: true でコンパイルした場合
// 標準方式の書き方(value と context)のデコレーター
function log(target: Function, context: ClassMethodDecoratorContext) {
console.log(String(context.name));
}
function sealed(target: Function, context: ClassDecoratorContext) {}
function prop(target: undefined, context: ClassFieldDecoratorContext) {}
@sealed
class Cart {
@prop
name = "";
@log
total(): number { return 0; }
accessor stock = 0;
}
std_in_legacy.ts(8,1): error TS1238: Unable to resolve signature of class decorator when called as an expression.
The runtime will invoke the decorator with 1 arguments, but the decorator expects 2.
std_in_legacy.ts(10,4): error TS1240: Unable to resolve signature of property decorator when called as an expression.
Argument of type 'Cart' is not assignable to parameter of type 'undefined'.
std_in_legacy.ts(13,4): error TS1241: Unable to resolve signature of method decorator when called as an expression.
Type 'Cart' is missing the following properties from type 'Function': apply, call, bind, prototype, and 4 more.
直し方は、tsconfig から experimentalDecorators を外すことです。
戻り値の型が合わないとき(TS1270)
標準方式で、元のメソッドと違う型の関数を返すと TS1270 になります。
function toStr(target: (this: any) => number, context: ClassMethodDecoratorContext) {
return function (this: any): string {
return String(target.call(this));
};
}
class C {
@toStr
size(): number { return 1; }
}
ret.ts(7,4): error TS1270: Decorator function return type '(this: any) => string' is not assignable to type 'void | (() => number)'.
Type '(this: any) => string' is not assignable to type '() => number'.
Type 'string' is not assignable to type 'number'.
置き換える関数は、元のメソッドと同じ引数・戻り値の型にします。 log の例のように型引数 Args と Return を使うと、どのメソッドに付けても型が合います。
エラー番号の早見表
| 番号 | 出た場面 | 見るところ | 直し方 |
|---|---|---|---|
| TS1238 | クラスデコレーター | 「1 arguments, but the decorator expects 2」 | 標準方式の関数を旧方式の設定で使っている。experimentalDecorators を外す |
| TS1240 | フィールド(プロパティ)デコレーター | 第1引数の型の食い違い | 方式に合わせて引数の形を直す |
| TS1241 | メソッドデコレーター | 「2 arguments, but the decorator expects 3」 | 旧方式の関数を標準方式で使っている。設定か関数を直す |
| TS1270 | 標準方式で関数を返したとき | 戻り値の型の食い違い | 元のメソッドと同じ型の関数を返す |
| TS1206 | 関数宣言・constructor・引数に付けたとき | Decorators are not valid here. | クラスとそのメンバーに付ける。引数に付けるなら旧方式 |
| TS8038 | export の前後両方に書いたとき | Decorators may not appear after 'export' … | export の前か後ろのどちらかにまとめる |
| TS5052 | emitDecoratorMetadata だけを指定 | Option 'emitDecoratorMetadata' cannot be specified without specifying option 'experimentalDecorators'. | experimentalDecorators と一緒に指定する |
| TS2550 | lib なしで Symbol.metadata を使ったとき | Property 'metadata' does not exist on type 'SymbolConstructor'. | lib に esnext.decorators を加える |
TS1206 と TS8038 は、次のコードで出たものです。
function deco(value: unknown, context: DecoratorContext) {}
@deco
function hello() {}
class A {
@deco
constructor() {}
}
@deco export @deco class B {}
misc.ts(3,1): error TS1206: Decorators are not valid here.
misc.ts(7,3): error TS1206: Decorators are not valid here.
misc.ts(11,14): error TS8038: Decorators may not appear after 'export' or 'export default' if they also appear before 'export'.
Symbol.metadata でクラスに情報を持たせる(TS 5.2以降)
標準方式には、デコレーターからクラスに情報を残す仕組みがあります。context.metadata に書き込んだ内容を、あとで クラス[Symbol.metadata] から読み出せます。 フィールドに表示用のラベルを付ける例です。
tsconfig は、lib に esnext.decorators を加えます。lib を自分で指定すると console などの定義が外れるので、dom も加えています。
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.decorators", "dom"],
"module": "nodenext",
"strict": true,
"rootDir": ".",
"outDir": "dist"
}
}
function label(text: string) {
return function (_value: undefined, context: ClassFieldDecoratorContext) {
const labels = (context.metadata.labels ??= {}) as Record<string, string>;
labels[String(context.name)] = text;
};
}
class Member {
@label("氏名") name = "";
@label("メール") email = "";
}
console.log(Member[Symbol.metadata]);
[Object: null prototype] { labels: { name: '氏名', email: 'メール' } }
Node.js 22.22.2 と 24.21.0 のどちらでも、typeof Symbol.metadata は undefined でした。そのため1行目の代入(ポリフィル)が必要です。この行を消して実行すると、context.metadata が undefined になり、次のエラーで止まります。
TypeError: Cannot read properties of undefined (reading 'labels')
TypeScript 5.2 のリリースノートにも、Symbol.metadata のポリフィルと、lib に esnext か esnext.decorators を含める設定が案内されています。
lib を指定せずにコンパイルしたときのエラーは次のとおりです(Symbol.metadata に関する行を抜粋)。
meta.ts(15,27): error TS2550: Property 'metadata' does not exist on type 'SymbolConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'esnext' or later.
旧方式の emitDecoratorMetadata との違い
旧方式で emitDecoratorMetadata を有効にすると、tsc が型の情報を JavaScript に書き出します。次のコードをコンパイルしました。
function field(target: object, key: string) {}
class Member {
@field name: string = "";
@field age: number = 0;
}
出力 JS の該当部分です。
__decorate([
field,
__metadata("design:type", String)
], Member.prototype, "name", void 0);
__decorate([
field,
__metadata("design:type", Number)
], Member.prototype, "age", void 0);
型注釈(string・number)が String・Number という値として出力されるのが emitDecoratorMetadata の特徴です。標準方式の Symbol.metadata は、デコレーターが自分で書き込んだ値だけを持ち、型注釈は自動では入りません。TypeScript 5.0 のリリースノートでも、標準方式は emitDecoratorMetadata と互換性がなく、パラメータデコレーターも使えないと説明されています。
標準方式と旧方式のどちらを使うか
| 状況 | 選ぶ方式 | 理由 |
|---|---|---|
| 新しく自分でデコレーターを書く | 標準方式 | 追加の設定がなく、型(context の型名)がそろっている |
| 使うライブラリが experimentalDecorators を指定している | 旧方式 | ライブラリのデコレーターの引数の形に合わせる |
| 引数(パラメータ)にデコレーターを付けたい | 旧方式 | 標準方式では TS1206 になる |
| 型注釈の情報を実行時に使いたい | 旧方式+emitDecoratorMetadata | 標準方式には自動で型を書き出す仕組みがない |
| 代入のたびに検査したい | 標準方式の accessor | get・set・init をまとめて差し替えられる |
1つの tsconfig の中では、どちらか一方の方式だけが使えます。ライブラリのドキュメントに experimentalDecorators の指定があるかどうかを最初に確かめると、取り違えを避けられます。
よくある質問
Q. デコレーターを使うのに tsconfig の設定は必要ですか? A. 標準方式は設定なしで書けます。Node.js で実行するときは target を es2022 にして tsc に変換させます。旧方式は experimentalDecorators: true が必要です。
Q. 関数(function 宣言)にデコレーターは付けられますか? A. 付けられません。TS1206(Decorators are not valid here.)になります。付けられるのはクラスと、そのメソッド・getter・setter・フィールド・accessor です。
Q. フィールドのデコレーターで、代入された値を検査できますか? A. フィールドのデコレーターが加工できるのは初期値だけです。代入のたびに検査したいときは、フィールドに accessor キーワードを付けて、accessor デコレーターの set で検査します。
Q. 引数つきのデコレーター(@range(0, 99) など)はどう書きますか? A. 「デコレーターを返す関数」を書きます。外側の関数が引数を受け取り、内側の関数が (value, context) を受け取ります。この記事の range・register・wrap がその形です。
まとめ
- 標準方式は (value, context) の2引数、旧方式は (target, キー, descriptor)。クラス側の @ の書き方は同じでも、関数の形が違う
- 標準方式は新しい関数やオブジェクトを return して差し替える。フィールドは初期値を加工する関数、accessor は get・set・init を返す
- Node.js 22・24 で動かすときは、target を es2022 にして tsc で変換する。esnext のままだと @ が残り SyntaxError になる
- 評価順は、標準方式が式を先に全部評価してから、メソッド、フィールド、クラスの順に適用。同じメンバーの中では下から上
- TS1241 の「2 arguments / 3」、TS1238 の「1 arguments / 2」は方式の取り違えのサイン。experimentalDecorators の有無を確かめる
- パラメータデコレーターと emitDecoratorMetadata は旧方式だけ。Symbol.metadata は標準方式で、lib に esnext.decorators とポリフィルが必要
参考・出典
- TypeScript 5.0 リリースノート「Decorators」(typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html)
- TypeScript 5.2 リリースノート「Decorator Metadata」(typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html)
- TypeScript Handbook「Decorators」(旧方式の説明。typescriptlang.org/docs/handbook/decorators.html)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript デコレーターの使い方|標準と旧方式の違いと書き方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。