IlmHamroh
JavaScript Full-stack/15-qism. TypeScript39/47-dars21 daqiqa
Mundarija (30)

TypeScript dekoratorlari: TC39 standarti — @ bilan metod, maydon va klassga xulq qo'shish

Qisqacha: Dekorator — klass yoki uning a'zosi (metod, maydon, accessor) oldiga @nom qilib yoziladigan funksiya. U a'zoni va uning haqidagi ma'lumotni (context) oladi va o'rniga yangisini qaytarishi mumkin: masalan, metodni log yozadigan o'ram bilan almashtiradi. TypeScript 5.0 dan beri bu TC39 standarti bo'yicha ishlaydi. Lekin dekorator — o'chirib bo'lmaydigan sintaksis: Node 24 ham, Chrome 154 ham uni hali bajarmaydi, shuning uchun kod avval tsc (yoki boshqa transpiler) bilan oddiy JavaScript'ga aylantiriladi.

Bu darsda

  • @logged kabi metod dekoratorini yozasiz va uning turini to'g'ri berasiz.
  • context obyektidagi kind, name, static, private, addInitializer, metadata ni ishlatasiz.
  • Maydon, accessor va klass dekoratorlarini, dekorator fabrikasini yozasiz.
  • Dekoratorlar qaysi tartibda baholanishi va qo'llanishini haqiqiy chiqishdan o'qiysiz.
  • Nega Node 24 .ts dagi dekoratorni bajara olmasligini va tsc uni nimaga aylantirishini ko'rsata olasiz.

Oldin bilishingiz kerak: JS'da dizayn naqshlari (dekorator naqshi), Klasslar TypeScript'da, Generic cheklovlar va keyof, Build: tsc, bundler'lar va tsgo.

1. Nega bu kerak?

Dizayn naqshlari darsida dekorator naqshini ko'rdik: funksiyani o'zgartirmasdan, uni o'rab, yangi xulq qo'shish. logBilan(fn) — funksiya oladi, log yozadigan yangi funksiya qaytaradi.

«Bahor» oshxonasida Sardor Kitchen klassini yozdi. Jasur aka so'radi: "Har buyurtma qachon boshlanganini ko'raylik". Sardor har metodga console.log qo'shib chiqdi:

ts
class Kitchen {
  startOrder(dish: string): string {
    console.log("-> startOrder");
    return `${dish} tayyorlanmoqda`;
  }
  finishOrder(dish: string): string {
    console.log("-> finishOrder");
    return `${dish} tayyor`;
  }
}
console.log(new Kitchen().startOrder("Osh"));

Konsolda:

text
-> startOrder
Osh tayyorlanmoqda

Ishlaydi, lekin bir xil qator har metodda takrorlanadi. Ertaga "vaqtni ham o'lchaylik" desa — hamma metodni qayta tahrirlash kerak. Klass metodlarini logBilan(…) bilan o'rash esa noqulay: metod klass ichida e'lon qilinadi, uni o'rash uchun klassdan keyin qo'lda Kitchen.prototype.startOrder = … yozish kerak.

Dekorator sintaksisi aynan shu uchun: o'ramni metodning ustiga yozasiz.

ts
class Kitchen {
  @logged
  startOrder(dish: string): string { … }
}

Hayotiy o'xshatish: oshxonadagi taom yorlig'i. Idishga "achchiq" yoki "vegetarian" deb yopishtirilgan stiker taomning o'zini o'zgartirmaydi, lekin ofitsiant unga qarab boshqacha xizmat qiladi. Dekorator ham shunday — a'zoning ustiga yopishtiriladigan "yorliq", faqat bu yorliq orqasida kod ishlaydi.

2. Birinchi dekorator: @logged

2.1 Dekorator — oddiy funksiya

ts
function logged(
  value: Function,
  context: ClassMethodDecoratorContext,
) {
  return function (this: unknown, ...args: unknown[]) {
    console.log(`-> ${String(context.name)}`);
    return value.apply(this, args);
  };
}

class Kitchen {
  @logged
  startOrder(dish: string): string {
    return `${dish} tayyorlanmoqda`;
  }
}
console.log(new Kitchen().startOrder("Osh"));

Konsolda:

text
-> startOrder
Osh tayyorlanmoqda

Qatorma-qator:

  • logged — oddiy funksiya, ikki argument oladi. value — bezatilayotgan metodning o'zi. context — u haqidagi ma'lumot: nomi (context.name — "startOrder"), turi, statikmi.
  • U yangi funksiya qaytaradi. Klassda asl metod o'rniga shu turadi.
  • this: unknown — o'ram oddiy function (arrow emas): this chaqiruvdagi obyekt bo'lib qoladi va asl metodga apply bilan uzatiladi.
  • @logged — "logged ni shu metodga qo'lla". Qavs yo'q: biz funksiyani chaqirmayapmiz, uni beryapmiz.

Endi finishOrder ga ham faqat bitta @logged qatori qo'shiladi. Log mantiqi bitta joyda.

Maslahat: Bu blokda saytning «Ishga tushir» tugmasi yo'q — sababini «Dekorator qanday ishga tushadi» bo'limida ko'ramiz. «TS Playground'da ochish» ni bosing va u yerda Run — Playground kodni avval o'giradi, keyin bajaradi.

2.2 Turni aniq berish

Function va unknown[] — bo'sh turlar: o'ram natijasi any kabi bo'lib qoladi. To'g'ri yozuv — generics bilan. Metodning this, argumentlari va natijasi parametr bo'ladi:

ts
type Method<This, Args extends unknown[], R> =
  (this: This, ...args: Args) => R;

function logged<This, Args extends unknown[], R>(
  value: Method<This, Args, R>,
  context: ClassMethodDecoratorContext<This, Method<This, Args, R>>,
): Method<This, Args, R> {
  return function (this: This, ...args: Args): R {
    console.log(`-> ${String(context.name)}(${args.join(", ")})`);
    return value.call(this, ...args);
  };
}

class Kitchen {
  @logged
  startOrder(dish: string, count: number): string {
    return `${count} ta ${dish} tayyorlanmoqda`;
  }
}
console.log(new Kitchen().startOrder("Manti", 3));

Konsolda:

text
-> startOrder(Manti, 3)
3 ta Manti tayyorlanmoqda

ClassMethodDecoratorContext — TypeScript'ning o'rnatilgan turi (lib ichida, import shart emas). Endi startOrder ning turi o'zgarmaydi: (dish: string, count: number) => string. O'ram noto'g'ri tur qaytarsa, tsc buni ko'radi.

2.3 Dekoratorni noto'g'ri joyga qo'yish

Metod dekoratorini maydonga qo'ysak nima bo'ladi?

ts
type Method<This, Args extends unknown[], R> =
  (this: This, ...args: Args) => R;

function logged<This, Args extends unknown[], R>(
  value: Method<This, Args, R>,
  context: ClassMethodDecoratorContext<This, Method<This, Args, R>>,
): Method<This, Args, R> {
  return function (this: This, ...args: Args): R {
    return value.call(this, ...args);
  };
}

class Order {
  @logged
  total = 0;
}

Kompilyator:

text
TS1240: Unable to resolve signature of property decorator when called as an expression.
  Argument of type 'undefined' is not assignable to parameter of type 'Method<Order, unknown[], unknown>'.
TS1270: Decorator function return type 'Method<Order, unknown[], unknown>' is not assignable to type 'void | ((this: Order, value: number) => number)'.
  Type 'Method<Order, unknown[], unknown>' is not assignable to type '(this: Order, value: number) => number'.
    Type 'unknown' is not assignable to type 'number'.

Uzun, lekin xato xabarlarini pastdan o'qish usuli bilan tushunarli. TS1240: "xususiyat (property) dekoratorining imzosini aniqlab bo'lmadi" — davomi sababni aytadi: "undefined ni metod kutilgan parametrga berib bo'lmaydi". Ya'ni maydon dekoratori birinchi argument sifatida undefined oladi (maydonda hali qiymat yo'q), bizning logged esa metod kutadi. TS1270: "dekorator qaytargan tur mos emas" — maydon dekoratori faqat void yoki "boshlang'ich qiymatni o'zgartiruvchi" funksiya qaytarishi mumkin. Xulosa: tur yozilgan dekorator noto'g'ri joyga qo'yilsa, tsc darhol to'xtatadi. Function bilan yozilgan birinchi variantimiz esa bu xatoni ko'rmasdi.

Tekshirib ko'ring: Nega o'ram function (this: This, …) deb yozildi, arrow funksiya emas?

Javob

Arrow funksiyaning o'z this i yo'q — u logged chaqirilgan joydagi this ni oladi (u yerda undefined). O'ram esa klass nusxasi ustida chaqiriladi: kitchen.startOrder(). this aynan shu nusxa bo'lishi va asl metodga value.call(this, …) bilan uzatilishi kerak. Buning uchun oddiy function kerak (this qoidalari).

3. Dekorator qanday ishga tushadi

3.1 Node 24 va Chrome 154 — hali yo'q

Dekorator sintaksisi TypeScript'ning o'zinikimi? Yo'q. Bu JavaScript'ning kelajakdagi imkoniyati. TC39 (JavaScript standartini yozadigan qo'mita) taklifi 3-bosqichda — "tayyor, brauzerlar joriy qilishi kutilmoqda". Lekin joriy qilinmagan. Biz tekshirdik:

text
$ node index.ts        # Node 24.21.0
class Menu { @logged show() { return "osh"; } }
             ^

SyntaxError: Invalid or unexpected token

Chrome 154.0.8037.98 da ham xuddi shu: SyntaxError: Invalid or unexpected token. V8 da --js-decorators bayrog'i bor, lekin u "in progress" (ishlanmoqda) deb belgilangan va Node 24 da yoqsangiz ham kod baribir ishlamadi.

O'rnatish darsida Node .ts faylni turlarini o'chirib bajarishini ko'rdik. Dekoratorni o'chirib bo'lmaydi: @logged — tur emas, bajariladigan kod. Uni olib tashlasangiz, log yo'qoladi. Shuning uchun dekorator ham, xuddi enum kabi, o'chirib bo'lmaydigan (non-erasable) sintaksis.

Diqqat: erasableSyntaxOnly bayrog'i (bizning kurs/mashqlar/15/tsconfig.json da yoqiq) enum ni ushlaydi, lekin dekoratorni ushlamaydi. Biz sinadik: tsc dekoratorli faylda jim (chiqish kodi 0), node fayl.ts esa SyntaxError beradi. Sabab: dekorator — JavaScript taklifi, TypeScript'ning maxsus sintaksisi emas. Bayroq faqat "TypeScript'ga xos" narsalarni taqiqlaydi.

3.2 tsc dekoratorni nimaga aylantiradi

Demak dekoratorli kodni bajarish uchun uni avval o'girish kerak — tsc, esbuild yoki boshqa transpiler bilan (Build: tsc, bundler'lar va tsgo). tsc 7.0.2 --target es2024 bilan eng kichik misolni shunday yozdi (yordamchi funksiyalar qisqartirildi):

js
var __esDecorate = function (ctor, descriptorIn, decorators,
  contextIn, initializers, extraInitializers) { /* 30 qator */ };
var __runInitializers = function (thisArg, initializers, value) {
  /* … */ };

let Menu = (() => {
  let _instanceExtraInitializers = [];
  let _show_decorators;
  return class Menu {
    static {
      _show_decorators = [logged];
      __esDecorate(this, null, _show_decorators, { kind: "method",
        name: "show", static: false, private: false,
        access: { has: obj => "show" in obj, get: obj => obj.show },
        metadata: _metadata }, null, _instanceExtraInitializers);
    }
    show() { return "osh"; }
    constructor() {
      __runInitializers(this, _instanceExtraInitializers);
    }
  };
})();

Uch narsaga qarang:

  • @logged yo'qoldi, o'rnida _show_decorators = [logged] va __esDecorate(…) chaqiruvi. Bu yordamchi standartdagi qoidalarni qo'lda bajaradi: logged ni chaqiradi va metodni u qaytargan funksiya bilan almashtiradi.
  • { kind: "method", name: "show", static: false, … } — bu aynan context obyekti. Endi u qayerdan kelishini ko'rdingiz.
  • Fayl tepasiga 40 ga yaqin qator yordamchi kod qo'shildi. Dekorator bepul emas: har dekoratorli klass biroz kattaroq va sekinroq yaratiladi.

--target esnext bilan esa tsc dekoratorni o'zgartirmaydi — @logged JavaScript'da qoladi. Kelajakda brauzerlar uni bajarganda shunday bo'ladi. Hozircha target ni es2024 yoki pastroq qo'ying. Xuddi shunday, esbuild'ga target berilmasa, u ham dekoratorni o'zgartirmaydi. tsx esa Node versiyasini biladi va uni pastga tushiradi — npx tsx@4.23.15 fayl.ts dekoratorli faylni bajardi.

Tekshirib ko'ring: tsc --noEmit dekoratorli faylda xato bermadi. node fayl.ts ishlaydimi?

Javob

Yo'q — SyntaxError: Invalid or unexpected token. tsc --noEmit faqat turlarni tekshiradi va hech narsa yozmaydi. Node esa turlarni o'chiradi, lekin dekoratorni o'chira olmaydi, va V8 uni hali tanimaydi. Ishga tushirish uchun tsc bilan dist/ ga yig'ib (target: "es2024"), node dist/fayl.js ni bajaring yoki tsx ishlating.

4. context obyekti

4.1 Har dekoratorga nima keladi

Har dekorator ikkinchi argument sifatida context oladi. Uning maydonlari:

Maydon Ma'nosi
kind "class", "method", "getter", "setter", "field", "accessor"
name a'zo nomi (yopiq a'zo uchun "#nom")
static, private statikmi, # bilan yopiqmi (klassda yo'q)
access a'zoni nusxadan o'qish/yozish: get(obj), set(obj, v)

Yana ikkitasi — addInitializer va metadata — quyida alohida. Bitta dekoratorni hamma joyga qo'yib, kind ni ko'ramiz:

ts
function inspect(_value: unknown, context: DecoratorContext): void {
  const { kind, name } = context;
  const isStatic = "static" in context ? context.static : "-";
  console.log(kind, String(name), isStatic);
}

@inspect
class Kitchen {
  @inspect static open(): void {}
  @inspect cook(): void {}
  @inspect dishes = 0;
  @inspect accessor chef = "Sardor";
  @inspect get busy(): boolean { return this.dishes > 5; }
  @inspect #secret = "ziravor";
}
console.log(typeof Kitchen);

Konsolda:

text
method open true
method cook false
accessor chef false
getter busy false
field dishes false
field #secret false
class Kitchen -
function
  • DecoratorContext — hamma kontekst turlarining unioni. Klass kontekstida static yo'q, shuning uchun "static" in context bilan toraytirdik.
  • #secret ham bezatildi: dekorator yopiq a'zoga ham qo'yiladi (name — "#secret").
  • Tartibga qarang: chiqish kod yozilgan tartibda emas. Avval metodlar va accessor'lar, keyin maydonlar, eng oxirida klass. Bu haqda «Bajarilish tartibi» bo'limida.

4.2 addInitializer — nusxa yaratilganda ishlaydigan kod

Klassik muammo: metodni o'zgaruvchiga olsangiz, this yo'qoladi (this qoidalari):

ts
class Waiter {
  name = "Bobur";
  greet(): string { return `Salom, men ${this.name}`; }
}
const { greet } = new Waiter();
console.log(greet());

Konsolda:

text
TypeError: Cannot read properties of undefined (reading 'name')

TypeScript buni ushlamadi — tur xatosi yo'q, xato faqat ishlash paytida. Dekorator bilan tuzatamiz: context.addInitializer(fn) — fn har yangi nusxa yaratilganda (konstruktor ichida) bajariladi. Unda metodni nusxaga bog'lab (bind) qo'yamiz:

ts
type Method<This, Args extends unknown[], R> =
  (this: This, ...args: Args) => R;

function bound<This extends object, Args extends unknown[], R>(
  value: Method<This, Args, R>,
  context: ClassMethodDecoratorContext<This, Method<This, Args, R>>,
): void {
  if (context.private) throw new TypeError("yopiq metod");
  context.addInitializer(function (this: This) {
    Reflect.set(this, context.name, value.bind(this));
  });
}

class Waiter {
  name = "Bobur";
  @bound greet(): string { return `Salom, men ${this.name}`; }
}
const { greet } = new Waiter();
console.log(greet()); // Salom, men Bobur

bound hech narsa qaytarmaydi (void) — metodning o'zi o'zgarmaydi. U faqat har nusxaga "bog'langan" nusxasini qo'yadi. Reflect.set(obj, nom, qiymat) — obj[nom] = qiymat ning funksiya ko'rinishi: context.name ning turi string | symbol, oddiy this[nom] yozuvini tsc qabul qilmasdi.

4.3 metadata — hozircha bo'sh

Standartda context.metadata ham bor: bitta klassning hamma dekoratorlari umumiy obyektga yozishi uchun. Natija klassning Symbol.metadata xususiyatida saqlanadi. Lekin Symbol.metadata ning o'zi ham hali yangi. Node 24.21.0 da typeof Symbol.metadata — "undefined". tsc chiqishi esa metadata ni faqat Symbol.metadata bor bo'lsa yaratadi. Shuning uchun Node 24 da context.metadata — undefined (sinab ko'rdik). Ishlatish uchun polyfill kerak (Symbol.metadata ??= Symbol("Symbol.metadata") kabi qator). Freymvorklar buni o'zi qiladi.

5. Dekorator turlari

5.1 Maydon dekoratori

Maydon dekoratori birinchi argument sifatida undefined oladi — maydon hali yaratilmagan. U funksiya qaytarishi mumkin: bu funksiya boshlang'ich qiymatni oladi va o'rniga boshqasini beradi.

ts
// Maydon dekoratori: boshlang'ich qiymatni o'zgartiradi
function trimmed<This>(
  _value: undefined,
  context: ClassFieldDecoratorContext<This, string>,
) {
  return function (this: This, initial: string): string {
    console.log(`${String(context.name)}: "${initial}"`);
    return initial.trim();
  };
}

class Guest {
  @trimmed name = "  Dilshod aka ";
}
console.log(`[${new Guest().name}]`);

Konsolda:

text
name: "  Dilshod aka "
[Dilshod aka]

ClassFieldDecoratorContext<This, string> — "faqat string maydonga". Uni count = 0 ga qo'ysangiz, tsc to'xtatadi. Cheklov: maydon dekoratori faqat boshlang'ich qiymatni ko'radi. Keyinroq guest.name = " Ali " deb yozilsa — u bilmaydi. Har yozishni kuzatish uchun accessor kerak.

5.2 accessor — avtomatik getter/setter

accessor — dekoratorlar bilan birga kelgan yangi kalit so'z. accessor price = 0 — oddiy maydonga o'xshaydi, lekin ichida yashirin #price maydoni va uni o'qiydigan/yozadigan getter va setter bor. Dekorator shu getter/setter'ni almashtira oladi:

ts
// Accessor dekoratori: manfiy narxni qabul qilmaydi
function nonNegative<This>(
  target: ClassAccessorDecoratorTarget<This, number>,
  context: ClassAccessorDecoratorContext<This, number>,
): ClassAccessorDecoratorResult<This, number> {
  const name = String(context.name);
  return {
    set(this: This, value: number) {
      if (value < 0) {
        throw new RangeError(`${name} manfiy: ${value}`);
      }
      target.set.call(this, value);
    },
  };
}

class Dish {
  @nonNegative accessor price = 0;
}
const osh = new Dish();
osh.price = 35000;
console.log(osh.price);
osh.price = -1;

Konsolda:

text
35000
RangeError: price manfiy: -1
  • target — asl get va set. Biz faqat set ni almashtirdik: avval tekshiruv, keyin asl target.set.
  • Qaytgan obyektda get, set va init (boshlang'ich qiymat uchun) bo'lishi mumkin — keraklisini yozasiz.
  • Bu himoya ishlash paytida ishlaydi — TypeScript turi emas. price: number turi -1 ni o'tkazib yuboradi, dekorator esa to'xtatadi.

5.3 Klass dekoratori

Klass dekoratori klassning o'zini oladi. U klassni boshqasi bilan almashtirishi mumkin, lekin ko'pincha shunchaki uni biror joyga ro'yxatga oladi. Bunday dekoratorni parametr bilan yozamiz — bu dekorator fabrikasi (factory): dekorator qaytaradigan funksiya. @station("osh") dagi qavslar — fabrikani chaqirish:

ts
type Station = new () => { prepare(): string };
const stations = new Map<string, Station>();

function station(name: string) {
  return (cls: Station, context: ClassDecoratorContext<Station>) => {
    context.addInitializer(() => {
      stations.set(name, cls);
    });
  };
}

@station("osh")
class OshStation {
  prepare(): string { return "Osh tayyor"; }
}

@station("manti")
class MantiStation {
  prepare(): string { return "Manti bug'da"; }
}

for (const [name, Cls] of stations) {
  console.log(name, "→", new Cls().prepare());
}

Konsolda:

text
osh → Osh tayyor
manti → Manti bug'da

Station — "argumentsiz new bilan yaratiladigan va prepare() metodi bor klass" turi. Klass dekoratoridagi addInitializer klass to'liq e'lon qilinganidan keyin bir marta ishlaydi. Natija: oshxona bo'limlari o'zlarini o'zlari ro'yxatga yozdi, stations ni qo'lda to'ldirish shart emas. Aynan shu g'oya — "klassni belgilab, freymvork o'zi topadi" — keyingi darsdagi NestJS va Angular'ning asosi.

Oldingi logged ni ham fabrikaga aylantirish oson: function logged(prefix: string) { return function (value, context) { … } } — va @logged("kassa"). Qoida: @nom — tayyor dekorator, @nom(…) — dekorator qaytaradigan fabrika.

Tekshirib ko'ring: @station (qavssiz) deb yozsangiz nima bo'ladi?

Javob

station funksiyasi klass dekoratori sifatida chaqiriladi: birinchi argument — klass, ikkinchisi — context. Lekin u bitta name: string kutadi va dekorator qaytaradi. tsc darhol xato beradi — TS1238: Unable to resolve signature of class decorator when called as an expression., davomi: "typeof OshStation ni string kutgan parametrga berib bo'lmaydi". Turlarsiz JavaScript'da esa jim xato bo'lardi: klass Map ga umuman yozilmasdi.

6. Bajarilish tartibi

Bitta a'zoda bir nechta dekorator bo'lishi mumkin: @a @b method(). Qaysi biri birinchi? Ikki bosqich bor — baholash (@ dan keyingi ifoda hisoblanadi, fabrika chaqiriladi) va qo'llash (dekorator funksiyasi a'zoga qo'llanadi). Haqiqiy chiqishdan ko'ramiz:

ts
function trace(label: string) {
  console.log(`baholandi: ${label}`);
  return function (_value: unknown, context: DecoratorContext) {
    console.log(`qo'llandi: ${label} (${context.kind})`);
  };
}

@trace("klass")
class Kitchen {
  @trace("A") @trace("B")
  cook(): void {}

  @trace("maydon")
  dishes = 0;

  @trace("static")
  static open(): void {}
}
console.log("tayyor:", new Kitchen().dishes);

Konsolda:

text
baholandi: klass
baholandi: A
baholandi: B
baholandi: maydon
baholandi: static
qo'llandi: static (method)
qo'llandi: B (method)
qo'llandi: A (method)
qo'llandi: maydon (field)
qo'llandi: klass (class)
tayyor: 0
flowchart TB
  A["1. Baholash<br/>yuqoridan pastga"] --> B["2. Statik metodlar"]
  B --> C["3. Nusxa metodlari<br/>pastki @ birinchi"]
  C --> D["4. Maydonlar"]
  D --> E["5. Klass — oxirida"]

Qoidalar:

  1. Baholash — kod yozilgan tartibda, yuqoridan pastga, chapdan o'ngga: klass, A, B, maydon, static.
  2. Qo'llash — bitta a'zoda pastdan yuqoriga (ichkaridan tashqariga): B metodga yaqinroq, u birinchi. Xuddi o'ramni o'rash: A(B(cook)).
  3. A'zolar orasida: avval statik metodlar, keyin nusxa metodlari (accessor'lar ham shu yerda), keyin maydonlar, eng oxirida klass.

Amalda bu tartib bir nechta dekorator bir-biriga ta'sir qilganda muhim. Masalan @logged @bound greet() da bound birinchi qo'llanadi va logged qaytargan o'ramni emas, asl metodni bog'laydi.

7. Ko'p uchraydigan xatolar

7.1 TS1206: Decorators are not valid here.

Dekorator faqat klass va uning a'zolariga qo'yiladi. Oddiy funksiyaga yoki o'zgaruvchiga — yo'q:

ts
function logged(value: Function, context: DecoratorContext) {}

@logged
function pay(): void {}

Kompilyator:

text
TS1206: Decorators are not valid here.

Tarjimasi: "Dekoratorlar bu yerda yaroqsiz". Oddiy funksiyani o'rash uchun dekorator naqshining o'zi — const loggedPay = logBilan(pay) — yetarli.

7.2 TS1241 — dekorator imzosi mos emas

ts
function wrong(x: number) {}

class Menu {
  @wrong
  show(): void {}
}

Kompilyator:

text
TS1241: Unable to resolve signature of method decorator when called as an expression.
  Argument of type '() => void' is not assignable to parameter of type 'number'.

Tarjimasi: "metod dekoratorining imzosini ifoda sifatida chaqirilganda aniqlab bo'lmadi". Davomi aniqroq: "() => void (metod) ni number kutgan parametrga berib bo'lmaydi". Ko'pincha sabab — fabrikani qavssiz yozish: @station o'rniga @station("osh") kerak edi.

7.3 tsc jim, Node SyntaxError

node fayl.ts dekoratorni bajarmaydi (erasableSyntaxOnly ham ogohlantirmaydi). Tuzatish: tsc bilan target: "es2024" ga yig'ing yoki tsx ishlating.

7.4 Eski uslubdagi dekoratorni yangi bilan aralashtirish

Internetdagi ko'p misollar (target, propertyKey, descriptor) — uch argumentli. Bu eski, "legacy" dekoratorlar — experimentalDecorators bayrog'i bilan ishlaydi. Standart dekoratorga esa ikki argument keladi: (value, context). Ikkalasini bitta loyihada aralashtirib bo'lmaydi — keyingi darsda batafsil.

8. Mashqlar

1-mashq (oson): Tartibni ayting

ts
class Cashier {
  @first @second
  pay(): void {}
}

first va second — oddiy (fabrika emas) dekoratorlar, har biri qo'llanganda o'z nomini chiqaradi. Konsolda birinchi chiqadigan nom: [:second]. Ikkinchisi: [:first].

Yechim

Bitta a'zodagi dekoratorlar pastdan yuqoriga — metodga yaqinidan boshlab qo'llanadi: avval second, keyin first. Natija first(second(pay)) ga teng: first tashqi o'ram. Agar ular fabrika bo'lganida (@first()), baholash yuqoridan pastga bo'lardi — "baholandi" qatorlari teskari tartibda chiqardi.

2-mashq (o'rta): @timed — metod qancha vaqt oldi

@timed metod dekoratorini yozing: metod tugagach, startOrder: 2 ms kabi qator chiqarsin. Vaqt uchun performance.now() (Performans o'lchash). Turni logged dagi kabi generic bilan bering. Metod xato tashlasa ham vaqt chiqsin — try/finally dan foydalaning (throw va Error).

Yechim
ts
type Method<This, Args extends unknown[], R> =
  (this: This, ...args: Args) => R;

function timed<This, Args extends unknown[], R>(
  value: Method<This, Args, R>,
  context: ClassMethodDecoratorContext<This, Method<This, Args, R>>,
): Method<This, Args, R> {
  const name = String(context.name);
  return function (this: This, ...args: Args): R {
    const start = performance.now();
    try {
      return value.call(this, ...args);
    } finally {
      const ms = Math.round(performance.now() - start);
      console.log(`${name}: ${ms} ms`);
    }
  };
}

class Kitchen {
  @timed
  countDishes(orders: readonly string[]): number {
    return orders.length;
  }
}
const count = new Kitchen().countDishes(["Osh", "Manti"]);
console.log("taomlar:", count);

Konsolda:

text
countDishes: 0 ms
taomlar: 2

finally bloki return dan keyin ham, xato tashlanganda ham bajariladi — vaqt har holda chiqadi. Tez metodda 0 ms (yaxlitlangan) chiqishi tabiiy. Async metodlar uchun bu yetmaydi: value.call Promise qaytaradi va finally u tugashini kutmaydi. Async kodni tiplash — Async kod va xatolarni tiplash darsida.

3-mashq (qiyin): Amaliy tajriba — @minLength va build

kurs/mashqlar/15/39-dekorator/ — alohida build loyihasi: dekoratorli .ts ni Node bajarmaydi, shuning uchun bu yerda tsc dist/ ga yozadi. Ildizdagi tsconfig.json ning exclude iga "39-dekorator" ni qo'shing.

  1. 39-dekorator/tsconfig.json: strict, target: "es2024", module: "nodenext", rootDir: "src", outDir: "dist", types: ["node"].
  2. src/guest.ts: minLength(n) — accessor dekoratori fabrikasi. Satr n dan qisqa bo'lsa RangeError("name kamida 3 belgi: Al") tashlasin. Tekshiruv har yozishda (set) ishlasin — konstruktordagi this.name = name ham setter orqali o'tadi. Guest klassi: @minLength(3) accessor name: string, konstruktor ismni oladi.
  3. test/guest.test.js — ../dist/guest.js ni import qilib, uch test: to'g'ri ism, qisqa ism konstruktorda, qisqa ism yozishda.
  4. package.json (39-dekorator ichida, "type": "module"): "build": "tsc", "test": "npm run build && node --test test/guest.test.js" — node --test ga papka emas, fayl beriladi.
Yechim

src/guest.ts:

ts
// 39-dekorator/src/guest.ts — mehmon ismi kamida n belgi
export function minLength(min: number) {
  return function <This>(
    target: ClassAccessorDecoratorTarget<This, string>,
    context: ClassAccessorDecoratorContext<This, string>,
  ): ClassAccessorDecoratorResult<This, string> {
    const name = String(context.name);
    const check = (value: string): string => {
      if (value.length < min) {
        throw new RangeError(`${name} kamida ${min} belgi: ${value}`);
      }
      return value;
    };
    return {
      set(this: This, value: string) {
        target.set.call(this, check(value));
      },
    };
  };
}

export class Guest {
  @minLength(3) accessor name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const guest = new Guest("Malika");
console.log(guest.name);

Konsolda:

text
Malika

Konstruktordagi this.name = name setter orqali o'tadi — shuning uchun bitta set ham konstruktorni, ham keyingi yozishlarni tekshiradi.

Biz qilgan tajriba (TS ushlamagan tuzoq). Boshlang'ich qiymatni ham tekshirish uchun qaytgan obyektga init: check qo'shib ko'rdik. tsc jim — init ning turi (value: string) => string, check aynan shunday. Ishga tushirdik:

text
TypeError: Cannot read properties of undefined (reading 'length')

Sabab: accessor name: string; ga boshlang'ich qiymat yozilmagan, lekin init baribir chaqiriladi — undefined bilan. Standart bo'yicha initsializator har nusxada ishlaydi, qiymat bo'lmasa undefined oladi. Tur esa "string keladi" deb va'da beradi va bu yerda yolg'on gapiradi. Xulosa: init ishlatsangiz, undefined ni o'zingiz tekshiring yoki accessor ga doim boshlang'ich qiymat bering.

test/guest.test.js:

js
import { test } from "node:test";
import assert from "node:assert/strict";
import { Guest } from "../dist/guest.js";

test("to'g'ri ism saqlanadi", () => {
  assert.equal(new Guest("Bobur").name, "Bobur");
});

test("qisqa ism konstruktorda — RangeError", () => {
  assert.throws(() => new Guest("Al"), RangeError);
});

test("qisqa ism yozishda — RangeError", () => {
  const guest = new Guest("Malika");
  assert.throws(() => { guest.name = "M"; }, /kamida 3 belgi/);
  assert.equal(guest.name, "Malika");
});

package.json skriptlari: "build": "tsc", "test": "npm run build && node --test test/guest.test.js". npm test ni ishga tushirganingizda (Node 24.21.0, tsc dan keyin) bizda shunday chiqdi — birinchi qator modul yuklanganda chiqadi:

text
Malika
✔ to'g'ri ism saqlanadi (1.6683ms)
✔ qisqa ism konstruktorda — RangeError (1.3262ms)
✔ qisqa ism yozishda — RangeError (0.3804ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 182.5787

Tajriba: node src/guest.ts ni ham ishga tushirib ko'ring — SyntaxError: Invalid or unexpected token. XATOLAR.md ga TS1240, TS1270, TS1206, TS1241 ni qo'shing va «Node 24: dekorator — SyntaxError, tsc bilan yig'ing» degan qatorni yozing. dist/ ni .gitignore ga qo'shib, kurs/mashqlar papkasidan:

bash
git add 15/39-dekorator 15/tsconfig.json 15/XATOLAR.md
git commit -m "15/39: minLength accessor dekoratori, tsc build va testlar"

9. Real ishda

  • Angular (Google'ning freymvorki) — @Component, @Injectable, @Input: butun freymvork dekoratorlar ustiga qurilgan. NestJS (Node server freymvorki) — @Controller, @Get. Ikkalasi hozircha asosan eski (legacy) dekoratorlardan foydalanadi — keyingi darsda ko'ramiz.
  • Lit (veb-komponentlar kutubxonasi) — @customElement, @property standart dekoratorlarni qo'llab-quvvatlaydi. MobX (holat boshqaruvi) 6-versiyadan standart @observable accessor ni qo'llaydi.
  • O'z kodingizda: log, vaqt o'lchash, bind, tekshiruv, keshlash — bir xil "o'ram"ni ko'p metodga qo'yish kerak bo'lganda. Bitta-ikkita joy uchun esa oddiy funksiya o'rami tushunarliroq (dizayn naqshlaridagi "qachon kerak emas" qoidasi).
  • Intervyu: "Dekorator nima va qanday ishlaydi?", "Standart va experimentalDecorators farqi?", "Dekoratorlar qaysi tartibda qo'llanadi?".

Xulosa

  • Dekorator — (value, context) oladigan funksiya; @nom bilan klass yoki a'zo ustiga yoziladi, a'zoni almashtirishi yoki unga kod qo'shishi mumkin.
  • context: kind, name, static, private, access, addInitializer (nusxa/klass yaratilganda), metadata (Node 24 da hali undefined).
  • Turlari: metod, getter/setter, maydon (boshlang'ich qiymat), accessor (get/set/init), klass. @nom(…) — fabrika.
  • Tartib: baholash yuqoridan pastga; qo'llash — bitta a'zoda pastdan yuqoriga, a'zolarda metodlar → maydonlar → klass.
  • Dekorator — o'chirib bo'lmaydigan JavaScript sintaksisi: Node 24 va Chrome 154 hali bajarmaydi, erasableSyntaxOnly ushlamaydi. tsc (target ≤ es2024) uni __esDecorate chaqiruvlariga aylantiradi.

Keyingi dars: Legacy dekoratorlar va reflect-metadata — NestJS, Angular va TypeORM ishlatadigan eski experimentalDecorators, parametr dekoratorlari va "dependency injection" g'oyasi.

Manbalar

  • TypeScript 5.0 Release Notes: "Decorators" (2023-03-16) — typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html
  • TC39 taklifi: "Decorators" (Stage 3) va "Decorator Metadata" — github.com/tc39/proposal-decorators
  • TypeScript hujjatlari: TSConfig erasableSyntaxOnly, experimentalDecorators — typescriptlang.org/tsconfig
  • Node.js hujjatlari: "Modules: TypeScript" (type stripping cheklovlari) — nodejs.org/docs/latest-v24.x/api/typescript.html
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
TypeScript dekoratorlari: TC39 standarti — @ bilan metod, maydon va klassga xulq qo'shish — IlmHamroh