IlmHamroh
JavaScript Full-stack/15-qism. TypeScript40/47-dars19 daqiqa
Mundarija (31)

TypeScript legacy dekoratorlar: experimentalDecorators, reflect-metadata va dependency injection

Qisqacha: Legacy dekoratorlar — TypeScript'ning 2015-yildan beri bor, standartdan oldingi dekoratorlari. Ular "experimentalDecorators": true bilan yoqiladi va boshqacha imzoga ega: metod dekoratori (target, key, descriptor) oladi, parametr dekoratorlari faqat shu rejimda bor. "emitDecoratorMetadata": true bilan tsc konstruktor parametrlarining turlarini JavaScript'ga yozib qo'yadi, reflect-metadata paketi esa ularni o'qiydi. NestJS, TypeORM kabi freymvorklar aynan shu bilan dependency injection (bog'liqliklarni avtomatik berish) qiladi.

Bu darsda

  • Legacy va standart dekoratorlarni imzosidan farqlaysiz va ularni aralashtirib bo'lmasligini bilasiz.
  • Legacy metod, maydon, parametr va klass dekoratorlarini o'qiy va yoza olasiz.
  • emitDecoratorMetadata tsc chiqishiga nima qo'shishini ko'rsatasiz.
  • reflect-metadata bilan konstruktor parametrlarining turlarini o'qib, kichik DI konteyner yozasiz.
  • Bu mexanizm qayerda jim buzilishini (interface, import type) bilasiz.

Oldin bilishingiz kerak: Standart dekoratorlar (TC39), Klasslar TypeScript'da, Modullar TypeScript'da, JS'da dizayn naqshlari.

1. Nega bu kerak?

O'tgan darsda standart dekoratorlarni o'rgandik. Endi Sardor internetda NestJS (Node server freymvorki, 24-qismdan keyin o'rganamiz — hozir bilish shart emas) misolini topdi:

ts
@Injectable()
export class OrderService {
  constructor(private readonly prices: PriceService) {}
}

Bu yerda sirli narsa bor: OrderService ni hech kim new OrderService(new PriceService()) deb yaratmaydi. Freymvork o'zi "bu klassga PriceService kerak ekan" deb biladi va beradi. Qanday biladi? Turlar ishlash paytida o'chib ketadi-ku (Nega TypeScript darsidagi type erasure)!

Javob — bu darsning mavzusi. NestJS standart dekoratorlardan emas, TypeScript'ning eski dekoratorlaridan foydalanadi. Ular bitta qo'shimcha imkoniyat beradi: tsc parametr turlarini JavaScript'ga yozib qoldiradi. 2026-yil oktabr holatiga ko'ra NestJS'ning rasmiy boshlang'ich loyihasi (nestjs/typescript-starter) tsconfig.json ida:

json
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,

va package.json da "reflect-metadata": "^0.2.2". Ish joyida bunday loyihaga albatta duch kelasiz. Kodni o'qiy olish uchun eski uslubni ham bilish kerak.

Hayotiy o'xshatish: restoran ta'minotchisi. Oshpaz "menga go'sht va guruch kerak" deb ro'yxat yozib qo'yadi, ta'minotchi esa ertalab hammasini o'zi olib keladi. Oshpaz bozorga bormaydi. DI konteyner — o'sha ta'minotchi, emitDecoratorMetadata — o'sha ro'yxat.

2. Legacy dekoratorlar

2.1 Yoqish va imzo

Legacy dekoratorlar tsconfig.json dagi bayroq bilan yoqiladi: "experimentalDecorators": true. Bayroq yoqilsa, hamma dekoratorlar eski qoidada ishlaydi — standart dekorator endi tanilmaydi.

Eski metod dekoratori uch argument oladi:

ts
// @ts-options: {"experimentalDecorators": true}
function Log(
  target: object,
  key: string,
  descriptor: PropertyDescriptor,
) {
  const original = descriptor.value;
  descriptor.value = function (...args: unknown[]) {
    console.log(`-> ${key}(${args.join(", ")})`);
    return original.apply(this, args);
  };
}

class Kitchen {
  @Log
  cook(dish: string, count: number): string {
    return `${count} ta ${dish}`;
  }
}
console.log(new Kitchen().cook("Osh", 2));

Konsolda:

text
-> cook(Osh, 2)
2 ta Osh
  • target — klass prototipi (metod shu yerda yashaydi, class ichidan darsidagi kabi).
  • key — metod nomi.
  • descriptor — xususiyat tavsifchisi: value (metodning o'zi), writable, enumerable (Xususiyat deskriptorlari). Dekorator uni joyida o'zgartiradi yoki yangisini qaytaradi.

Standartdagi (value, context) bilan solishtiring. Bu yerda descriptor.value — any: turlar zaif, original.apply ni tsc tekshirmaydi. Standart dekoratorda esa Method<This, Args, R> bilan aniq tur berish mumkin edi. Bu eski uslubning kamchiliklaridan biri.

2.2 Ikki uslubni aralashtirib bo'lmaydi

O'tgan darsdagi tur yozilgan logged ni experimentalDecorators yoqilgan loyihaga olib keldik:

ts
// @ts-options: {"experimentalDecorators": true}
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 value;
}
class Kitchen {
  @logged
  cook(): void {}
}

Kompilyator:

text
TS1241: Unable to resolve signature of method decorator when called as an expression.
  Argument of type 'Kitchen' is not assignable to parameter of type 'Method<unknown, unknown[], unknown>'.
    Type 'Kitchen' provides no match for the signature '(this: unknown, ...args: unknown[]): unknown'.
TS1270: Decorator function return type 'Method<unknown, unknown[], unknown>' is not assignable to type 'void | TypedPropertyDescriptor<() => void>'.

Pastdan o'qiymiz: "Kitchen da (this, ...args) => … imzosi yo'q" — ya'ni birinchi argument sifatida metod emas, prototip (Kitchen obyekti) keldi. Bu eski imzo: (target, key, descriptor). Ikkinchi xabar ham shuni aytadi: eski metod dekoratori void yoki TypedPropertyDescriptor qaytarishi kerak. Qoida: bitta loyihada — bitta uslub. Bayroq kodning hamma joyidagi @ ma'nosini o'zgartiradi.

Standart (TS 5.0+) Legacy (experimentalDecorators)
Metod imzosi (value, context) (target, key, descriptor)
Parametr dekoratori yo'q bor
Tur metadatasi yo'q emitDecoratorMetadata
Kelajak JavaScript standarti faqat TypeScript

2.3 Maydon, parametr va klass dekoratorlari

To'rt xil legacy dekoratorni bitta klassda ko'ramiz:

ts
// @ts-options: {"experimentalDecorators": true}
function Field(target: object, key: string): void {
  console.log("maydon:", key, "| target:", target.constructor.name);
}
function Param(target: object, key: string | undefined, i: number) {
  console.log("parametr:", key ?? "constructor", i);
}
function Sealed(constructor: Function): void {
  console.log("klass:", constructor.name);
  Object.seal(constructor.prototype);
}

@Sealed
class Kitchen {
  @Field chef = "Rustam aka";
  constructor(@Param size: number) {}
  cook(dish: string, @Param count: number): string {
    return `${count} ta ${dish}`;
  }
}
console.log(new Kitchen(3).cook("Osh", 2));

Konsolda:

text
maydon: chef | target: Kitchen
parametr: cook 1
parametr: constructor 0
klass: Kitchen
2 ta Osh
  • Maydon dekoratori (target, key) oladi — qiymat ham, deskriptor ham yo'q. U faqat "shu nomli maydon bor" deb belgi qo'ya oladi. target — prototip (target.constructor.name — Kitchen).
  • Parametr dekoratori (target, key, index) — qaysi metodning nechanchi parametri. Konstruktorda key — undefined. U ham qiymatni o'zgartira olmaydi, faqat belgilaydi. NestJS'dagi @Body(), @Param("id") — shu tur.
  • Klass dekoratori konstruktor funksiyani oladi. Sealed prototipni muhrladi (Object.seal) — endi unga yangi metod qo'shib bo'lmaydi.
  • Tartib: a'zolar (yozilish tartibida, parametr dekoratorlari o'z metodi bilan), keyin konstruktor parametrlari, oxirida klass.

Parametr dekoratori standartda yo'q. Uni experimentalDecorators siz yozib ko'ramiz:

ts
function Inject(token: string) {
  return (target: object, key: string | undefined, index: number) => {
    console.log(`parametr ${index}: ${token}`);
  };
}
class OrderService {
  constructor(@Inject("PRICES") prices: unknown) {}
}

Kompilyator:

text
TS1206: Decorators are not valid here.

O'tgan darsdagi xabar: standart rejimda parametrga dekorator qo'yib bo'lmaydi. NestJS'ning @Inject(), @Body() lari standart dekoratorlarga o'tmasligining asosiy sabablaridan biri shu.

Tekshirib ko'ring: Eski loyihada @Log metod dekoratori (target, key, descriptor) deb yozilgan. Siz tsconfig.json dan experimentalDecorators ni olib tashladingiz. Nima bo'ladi?

Javob

@Log endi standart qoidada chaqiriladi: birinchi argument — metodning o'zi, ikkinchisi — context obyekti. Imzo mos kelmaydi va tsc TS1241 beradi. Turlarsiz JavaScript'da esa descriptor — undefined, descriptor.value qatori TypeError tashlardi. Ko'chish — har dekoratorni yangi imzoga qayta yozish demak.

3. emitDecoratorMetadata — turlarni JavaScript'ga yozib qoldirish

3.1 tsc nimani qo'shadi

emitDecoratorMetadata bayrog'i bilan tsc dekoratorli klass va a'zolar uchun ularning turlarini ham chiqishga yozadi. 40-di/src/bahor.ts dagi klass (mashqda to'liq yozamiz):

ts
@Injectable()
export class OrderService {
  readonly #prices: PriceList;
  constructor(prices: PriceList) {
    this.#prices = prices;
  }
}

tsc 7.0.2 chiqishi (dist/bahor.js, qisqartirilgan):

js
let OrderService = class OrderService {
  #prices;
  constructor(prices) {
    this.#prices = prices;
  }
};
OrderService = __decorate([
  Injectable(),
  __metadata("design:paramtypes", [PriceList])
], OrderService);

Mana sir: __metadata("design:paramtypes", [PriceList]). tsc konstruktordagi prices: PriceList turini ko'rdi va klassning o'zini (qiymatni) massivga yozdi. Endi bu ma'lumot ishlash paytida ham bor. Turlar o'chdi, lekin ularning "izi" qoldi.

Uch xil kalit yoziladi:

Kalit Nima yoziladi
design:paramtypes konstruktor (yoki metod) parametrlarining turlari
design:type maydon yoki metodning turi
design:returntype metod natijasining turi

3.2 Faqat qiymati bor turlar yoziladi

Tur o'chadigan narsa bo'lsa, tsc nima yozadi? Sinab ko'rdik:

ts
interface Prices { osh: number }

@Injectable()
class OrderService {
  constructor(prices: Prices, count: number, name: string) {}
}

Chiqishda:

js
__metadata("design:paramtypes", [Object, Number, String])

number → Number, string → String (JavaScript'dagi o'ram konstruktorlari). Prices esa interface — ishlash paytida u yo'q, shuning uchun Object. Ya'ni interfeys bo'yicha bog'liqlikni avtomatik topib bo'lmaydi: Object hech narsa demaydi. Shuning uchun DI freymvorklarida "xizmat" har doim klass, interfeys emas. Interfeys bilan ishlash kerak bo'lsa, NestJS kabi freymvorklar qo'lda token beradi: @Inject("PRICES") — mana parametr dekoratorining asosiy ishi.

3.3 reflect-metadata — o'qish uchun

__metadata yordamchisi Reflect.metadata(…) ni chaqiradi. Lekin bunday funksiya JavaScript'da yo'q: Reflect obyektida faqat standart metodlar bor (Proxy va Reflect). Uni reflect-metadata paketi (0.2.2) qo'shadi — bu "polyfill": hali standart bo'lmagan imkoniyatni qo'lda yozilgan kod bilan to'ldiradi.

tsc chiqishidagi yordamchiga qarang:

js
var __metadata = function (k, v) {
  if (typeof Reflect === "object" &&
      typeof Reflect.metadata === "function")
    return Reflect.metadata(k, v);
};

Reflect.metadata yo'q bo'lsa — jim hech narsa qilmaydi. Xato chiqmaydi, metadata shunchaki yozilmaydi. Shuning uchun import "reflect-metadata"; qatori dasturning eng boshida, birinchi dekoratorli klass yuklanishidan oldin bo'lishi shart.

Turlar tomoni ham shu paketda. Usiz tsc getMetadata ni tanimaydi:

ts
// @ts-options: {"experimentalDecorators": true}
function Injectable(): ClassDecorator {
  return () => {};
}

@Injectable()
class OrderService {
  constructor(name: string) {}
}
console.log(Reflect.getMetadata("design:paramtypes", OrderService));

Kompilyator:

text
TS2339: Property 'getMetadata' does not exist on type 'typeof Reflect'.

Tarjimasi: "typeof Reflect turida getMetadata xususiyati yo'q". import "reflect-metadata" qo'shilsa, paket Reflect turini declaration merging bilan kengaytiradi va xato yo'qoladi.

Tekshirib ko'ring: import "reflect-metadata" unutilgan, lekin tsc boshqa joyda turlarni olgan (masalan, boshqa faylda import bor). Dastur ishga tushganda qayerda buziladi?

Javob

Ikki joyda, ikkalasi ham noxush. __metadata jim ishlamaydi — metadata yozilmaydi. Keyin konteyner Reflect.getMetadata(…) ni chaqirganda — TypeError: Reflect.getMetadata is not a function (biz 3-mashqda haqiqatda ko'ramiz). Xato metadata yozilgan joyda emas, o'qilgan joyda chiqadi — sababni topish qiyinroq.

4. Dependency injection

4.1 G'oya

Dependency injection (DI, "bog'liqlikni ichkariga berish") — klass o'ziga kerakli obyektlarni o'zi yaratmaydi, ularni tashqaridan oladi:

ts
class PriceList {
  priceOf(dish: string): number {
    return dish === "osh" ? 35000 : 30000;
  }
}

class OrderService {
  readonly #prices: PriceList;
  constructor(prices: PriceList) {
    this.#prices = prices;
  }
  total(dishes: readonly string[]): number {
    return dishes.reduce((s, d) => s + this.#prices.priceOf(d), 0);
  }
}

const orders = new OrderService(new PriceList());
console.log(orders.total(["osh", "manti"])); // 65000

Bu ham DI — qo'lda: new OrderService(new PriceList()). Foydasi: testda PriceList o'rniga soxtasini berish mumkin (testlarda tikuv — vazifalar dagi apiKlient({ fetch }) ham shu g'oya). Muammosi: 30 ta xizmatli serverda new A(new B(new C(), new D())) zanjiri juda uzun bo'ladi.

DI konteyner zanjirni o'zi quradi:

flowchart TB
  A["resolve(OrderService)"] --> B["paramtypes:<br/>[PriceList]"]
  B --> C["resolve(PriceList)"]
  C --> D["new PriceList()"]
  D --> E["new OrderService(priceList)"]

Sxema bo'yicha: konteyner OrderService ning metadata'sini o'qiydi, unga PriceList kerakligini ko'radi, avval uni yaratadi (rekursiv, rekursiya kabi), keyin OrderService ni. Bitta resolve chaqiruvi — butun zanjir.

4.2 Kichik konteyner

20 qatorlik konteyner — NestJS yuragining soddalashtirilgan nusxasi. Blokning birinchi qismi — container.ts fayli. Oxirida esa uni ishlatib ko'ramiz: ikki xizmat @Injectable() bilan belgilangan (haqiqiy loyihada ular bahor.ts da turadi — mashqda to'liq yozamiz). Birinchi qatordagi ikki bayroq loyihada tsconfig.json da yoziladi:

ts
// @ts-options: {"experimentalDecorators": true, "emitDecoratorMetadata": true}
// 40-di/src/container.ts — kichik DI konteyner (NestJS g'oyasi)
import "reflect-metadata";

type Class<T = unknown> = new (...args: never[]) => T;

const registered = new Set<Class>();

export function Injectable(): ClassDecorator {
  return (target) => {
    registered.add(target as unknown as Class);
  };
}

export class Container {
  readonly #instances = new Map<Class, unknown>();

  resolve<T>(cls: Class<T>): T {
    const ready = this.#instances.get(cls);
    if (ready !== undefined) return ready as T;
    if (!registered.has(cls)) {
      throw new Error(`${cls.name} @Injectable() emas`);
    }
    const deps: Class[] =
      Reflect.getMetadata("design:paramtypes", cls) ?? [];
    const args = deps.map((dep) => this.resolve(dep));
    const instance = new cls(...(args as never[]));
    this.#instances.set(cls, instance);
    return instance;
  }
}

// --- Ishlatib ko'ramiz (loyihada — bahor.ts va main.ts) ---
@Injectable()
class PriceList {
  priceOf(dish: string): number {
    return dish === "osh" ? 35000 : 30000;
  }
}

@Injectable()
class OrderService {
  readonly #prices: PriceList;
  constructor(prices: PriceList) {
    this.#prices = prices;
  }
  total(dishes: readonly string[]): number {
    return dishes.reduce((s, d) => s + this.#prices.priceOf(d), 0);
  }
}

const orders = new Container().resolve(OrderService);
console.log(orders.total(["osh", "manti"])); // 65000

Qatorma-qator:

  • Class<T> — "new bilan chaqiriladigan va T yaratadigan narsa" turi. never[] argumentlar — "argumentlari qanday bo'lsa ham mos keladi".
  • @Injectable() — fabrika (o'tgan darsdagi kabi qavsli). Klassni ro'yxatga oladi. Konteyner faqat belgilangan klasslarni yaratadi — tasodifiy klass emas.
  • resolve — avval keshdan qidiradi: har klassdan bitta nusxa ("singleton", dizayn naqshlari). Keyin metadata'dan bog'liqliklarni oladi va har biri uchun o'zini chaqiradi.
  • ?? [] — konstruktorsiz klassda (PriceList) metadata umuman yozilmaydi: getMetadata undefined qaytaradi.
  • Uchta as — type assertion. Ishlash paytidagi metadata'ning turini tsc bilmaydi (getMetadata any qaytaradi), shuning uchun konteyner ichida turni o'zimiz kafolatlaymiz. Bu — DI kutubxonalarining odatiy "chegarasi": ichida bir necha as, tashqarida esa resolve(OrderService) aniq OrderService qaytaradi.

Endi blok oxiridagi foydalanishga qarang: new Container().resolve(OrderService) — va 65000. Hech qayerda new PriceList() yo'q — konteyner uni OrderService ning metadata'sidan o'zi topdi va yaratdi. Klasslar «G'oya» bo'limidagi qo'lda yozilgan variant bilan bir xil — farqi faqat @Injectable() qatorlarida.

4.3 Jim buziladigan joy: import type

Modullar darsida "faqat tur sifatida ishlatiladigan narsani import type bilan oling" dedik. DI'da bu qoida tuzoq:

ts
import type { OrderService } from "./bahor.ts";

@Injectable()
export class Cashier {
  readonly #orders: OrderService;
  constructor(orders: OrderService) {
    this.#orders = orders;
  }
}

tsc 7.0.2 — xatosiz (hatto verbatimModuleSyntax bilan ham). Chiqishda esa:

js
__metadata("design:paramtypes", [Function])

OrderService emas — Function! import type qatori JavaScript'da o'chadi, tsc klassning qiymatiga murojaat qila olmaydi va "qandaydir funksiya" deb yozadi. Konteyner Function ni yaratishga urinadi va Function @Injectable() emas xatosini beradi — xato Cashier da emas, bog'liqlik o'qilganda. NestJS hujjati ham shuni ogohlantiradi: DI orqali olinadigan klass oddiy import bilan olinsin.

Qoida: dekoratorli klassning konstruktor parametrlaridagi klasslar — oddiy import. Bu emitDecoratorMetadata ning yagona joyi, bu yerda "tur" aslida qiymat sifatida ishlatiladi.

4.4 DI'ning asosiy foydasi — test

Konteyner faqat qulaylik emas. DI'ning haqiqiy foydasi — klassni boshqa bog'liqlik bilan sinash mumkinligi. OrderService PriceList ni o'zi yaratmagani uchun, testda unga soxta narxlar ro'yxatini beramiz. TypeScript bu yerda yordam beradi: soxta obyekt PriceList ning shakliga mos bo'lishi shart (Strukturaviy tiplash):

ts
class PriceList {
  priceOf(dish: string): number {
    return dish === "osh" ? 35000 : 30000;
  }
}

class OrderService {
  readonly #prices: PriceList;
  constructor(prices: PriceList) {
    this.#prices = prices;
  }
  total(dishes: readonly string[]): number {
    return dishes.reduce((s, d) => s + this.#prices.priceOf(d), 0);
  }
}

// Testda: hamma taom 1000 so'm — hisoblash mantiqi alohida sinaladi
const fakePrices: PriceList = { priceOf: () => 1000 };
const orders = new OrderService(fakePrices);
console.log(orders.total(["osh", "manti", "choy"])); // 3000

fakePrices — klassdan yaratilmagan oddiy obyekt. Lekin unda priceOf(dish): number bor, demak u PriceList turiga mos. Soxta narxlar bilan total ning faqat o'z ishi — yig'ish — tekshirildi. Haqiqiy narx o'zgarsa ham bu test buzilmaydi. priceOf ni noto'g'ri yozsangiz (masalan, satr qaytarsa), tsc soxta obyektni ham to'xtatadi.

Konteyner bo'lsa-yu, bu imkoniyat bo'lmasa — DI'dan foyda kam. NestJS'da ham testlar aynan shunday: Test.createTestingModule ga haqiqiy xizmat o'rniga soxtasi beriladi.

Tekshirib ko'ring: OrderService konstruktorida this.#prices = new PriceList() deb yozilganida, yuqoridagi testni yozib bo'larmidi?

Javob

Yo'q. Klass bog'liqligini o'zi yaratsa, tashqaridan boshqasini berishning yo'li yo'q — test doim haqiqiy PriceList bilan ishlaydi. Narxlar bazadan yoki serverdan kelsa, test sekin va beqaror bo'ladi. DI — "yaratma, so'ra" qoidasi: bog'liqlik konstruktor argumenti bo'lsa, uni almashtirish oson.

5. Bugungi kun: qaysi birini tanlash

5.1 Parametr xususiyatlari va erasableSyntaxOnly

NestJS misolidagi constructor(private readonly prices: PriceService) {} — klasslar darsidagi parametr xususiyati. Bizning kurs/mashqlar/15 va vazifalar da erasableSyntaxOnly yoqiq va u buni taqiqlaydi:

ts
// @ts-options: {"erasableSyntaxOnly": true}
class PriceList {}
class OrderService {
  constructor(private prices: PriceList) {}
}

Kompilyator:

text
TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.

Tarjimasi: "erasableSyntaxOnly yoqilganda bu sintaksisga ruxsat yo'q". Parametr xususiyati konstruktorga this.prices = prices qatorini qo'shadi — o'chirib bo'lmaydi. Shuning uchun konteyner misolimizda readonly #prices va qo'lda yozilgan konstruktor bor. NestJS loyihasi esa tsc bilan yig'iladi va bu bayroqni yoqmaydi — u yerda parametr xususiyatlari odatiy.

5.2 Tanlov

  • Yangi kod, freymvorksiz — standart dekoratorlar (o'tgan dars). Ular JavaScript standartiga aylanmoqda va turlari aniqroq.
  • NestJS, TypeORM, Angular'ning eski versiyalari, InversifyJS, tsyringe — legacy: freymvork talab qiladi. experimentalDecorators + emitDecoratorMetadata + reflect-metadata.
  • Aralashtirish — yo'q: bayroq butun loyihaga ta'sir qiladi.
  • TypeScript 7.0 da ikkala uslub ham ishlaydi: TS 7.0 e'lonidagi olib tashlangan sozlamalar ro'yxatida experimentalDecorators yo'q. Biz 7.0.2 da sinadik — __decorate va __metadata chiqishi o'sha-o'sha.

Node 24 legacy dekoratorni ham bajarmaydi (o'sha SyntaxError): har ikki uslub tsc yoki boshqa transpiler orqali o'tadi.

6. Ko'p uchraydigan xatolar

6.1 TS1241 / TS1270 — uslublar aralashdi

Internetdan standart dekorator olib, legacy loyihaga qo'ydingiz (yoki teskarisi). Tuzatish: loyihaning experimentalDecorators holatiga qarang va dekoratorni shu imzoga moslang.

6.2 TS1206 — parametr dekoratori bayroqsiz

Tuzatish: "experimentalDecorators": true — agar freymvork talab qilsa. Aks holda parametr dekoratori o'rniga konstruktor argumentini oddiy funksiya bilan tekshiring.

6.3 Reflect.getMetadata is not a function

import "reflect-metadata" yo'q yoki juda kech. Tuzatish: kirish faylining (main.ts) birinchi qatori.

6.4 Bog'liqlik Object yoki Function bo'lib keldi

Konstruktor parametri interface (→ Object) yoki import type bilan olingan klass (→ Function). Tuzatish: klass va oddiy import; interfeys uchun — token (@Inject("…")).

6.5 TS1294 — parametr xususiyati

erasableSyntaxOnly yoqilgan loyihada constructor(private x: X). Tuzatish: #x maydon + konstruktorda this.#x = x, yoki loyiha type stripping'siz yig'ilsa — bayroqni o'chirish (jamoa qarori).

7. Mashqlar

1-mashq (oson): Qaysi uslub?

Har imzoni uslubi bilan juftlang: (a) (value, context); (b) (target, key, descriptor); (c) (target, key, index). Standart uslubdagisi — [:a]. Faqat legacy'da bor parametr dekoratori — [:c].

Yechim

(a) — standart (TS 5.0+, context obyekti). (b) — legacy metod dekoratori. (c) — legacy parametr dekoratori: index — parametrning o'rni. Standartda parametr dekoratori yo'q, shuning uchun (c) faqat experimentalDecorators bilan ishlaydi.

2-mashq (o'rta): Metadata'ni bashorat qiling

emitDecoratorMetadata bilan tsc quyidagi konstruktor uchun design:paramtypes ga nima yozadi?

ts
interface Menu { items: string[] }
class PriceList {}

@Injectable()
class Waiter {
  constructor(menu: Menu, prices: PriceList, tables: number[]) {}
}

Massivni to'ldiring: [Object, , ].

Yechim
js
__metadata("design:paramtypes", [Object, PriceList, Array])

Menu — interfeys, ishlash paytida yo'q: Object. PriceList — klass, qiymati bor: o'zi yoziladi. number[] — massiv: Array (element turi yo'qoladi). Biz tsc 7.0.2 da tekshirdik. Demak konteyner Waiter uchun faqat PriceList ni topa oladi — Menu va number[] uchun token yoki qo'lda berish kerak.

3-mashq (qiyin): Amaliy tajriba — 40-di/

kurs/mashqlar/15/40-di/ — alohida build loyihasi (39-dekorator/ kabi; ildiz tsconfig.json ning exclude iga "40-di" ni qo'shing).

  1. 40-di da: package.json ("type": "module"), npm install --save-exact reflect-metadata@0.2.2.
  2. tsconfig.json: strict, target: "es2024", module: "nodenext", rootDir: "src", outDir: "dist", types: ["node"], experimentalDecorators, emitDecoratorMetadata, rewriteRelativeImportExtensions.
  3. src/container.ts — darsdagi konteyner. src/bahor.ts — PriceList (priceOf(dish): osh 35 000, manti 30 000, boshqasi 0) va OrderService (total(dishes)), ikkalasi @Injectable().
  4. test/container.test.js — ../dist/… dan: zanjir o'zi yaratiladi (osh + manti = 65 000), bitta konteynerda bitta nusxa, @Injectable() siz klass — xato.
  5. Ikki tajriba: import "reflect-metadata" ni izohga oling; keyin uni qaytarib, import type bilan Cashier yozing. Nima bo'ladi?
Yechim

src/bahor.ts:

ts
// 40-di/src/bahor.ts — «Bahor» xizmatlari
import { Injectable } from "./container.ts";

@Injectable()
export class PriceList {
  priceOf(dish: string): number {
    const prices: Record<string, number> = {
      osh: 35000,
      manti: 30000,
    };
    return prices[dish] ?? 0;
  }
}

@Injectable()
export class OrderService {
  readonly #prices: PriceList;
  constructor(prices: PriceList) {
    this.#prices = prices;
  }
  total(dishes: readonly string[]): number {
    return dishes.reduce((s, d) => s + this.#prices.priceOf(d), 0);
  }
}

test/container.test.js:

js
import { test } from "node:test";
import assert from "node:assert/strict";
import { Container } from "../dist/container.js";
import { OrderService, PriceList } from "../dist/bahor.js";

test("bog'liqlik o'zi yaratiladi", () => {
  const orders = new Container().resolve(OrderService);
  assert.equal(orders.total(["osh", "manti"]), 65000);
});

test("bitta konteynerda — bitta nusxa", () => {
  const c = new Container();
  assert.equal(c.resolve(PriceList), c.resolve(PriceList));
});

test("@Injectable() siz klass — xato", () => {
  class Unknown {}
  assert.throws(() => new Container().resolve(Unknown), /Injectable/);
});

package.json skriptlari: "build": "tsc", "test": "npm run build && node --test test/container.test.js". npm test — bizda (Node 24.21.0, tsc 7.0.2) shunday chiqdi:

text
✔ bog'liqlik o'zi yaratiladi (3.3998ms)
✔ bitta konteynerda — bitta nusxa (0.4852ms)
✔ @Injectable() siz klass — xato (1.4769ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 367.6987

1-tajriba — import "reflect-metadata" izohda. tsc:

text
src/container.ts(24,15): error TS2339: Property 'getMetadata' does not exist on type 'typeof Reflect'.

Xato bo'lsa ham tsc dist/ ni yozdi (noEmitOnError yo'q — o'rnatish darsidagi tuzoq). Uni ishga tushirsak: TypeError: Reflect.getMetadata is not a function.

2-tajriba — import type { OrderService } bilan Cashier. tsc jim, chiqishda __metadata("design:paramtypes", [Function]), resolve(Cashier) esa Error: Function @Injectable() emas. Oddiy import { OrderService } bilan — [OrderService] va hammasi ishlaydi.

XATOLAR.md ga TS1206 (parametr dekoratori), TS2339 (getMetadata), TS1294 va ikki ishlash xatosini qo'shing. dist/ va node_modules/ — .gitignore da:

bash
git add 15/40-di 15/tsconfig.json 15/XATOLAR.md
git commit -m "15/40: kichik DI konteyner, reflect-metadata, testlar"

8. Real ishda

  • NestJS (Node server, 24-qismdan keyingi qismlarda): @Module, @Controller, @Injectable — hammasi legacy dekoratorlar va DI. Bugungi konteyner uning eng kichik modeli.
  • TypeORM (ma'lumotlar bazasi bilan ishlash kutubxonasi): @Entity(), @Column() — maydon dekoratorlari va design:type metadata'si. class-validator: @IsString(), @MinLength(3) — NestJS formalarni shu bilan tekshiradi.
  • Angular ham dekoratorlarga qurilgan, lekin o'z kompilyatori bor va reflect-metadata ga tayanmaydi.
  • Intervyu: "Dependency injection nima va nega kerak?", "NestJS klass turini qanday biladi?", "emitDecoratorMetadata nima yozadi?", "Interfeysni nega inject qilib bo'lmaydi?".

Xulosa

  • Legacy dekoratorlar — experimentalDecorators: metod (target, key, descriptor), maydon (target, key), parametr (target, key, index), klass (constructor). Standart bilan aralashtirib bo'lmaydi.
  • Parametr dekoratorlari va tur metadatasi — faqat legacy'da. Shuning uchun NestJS, TypeORM hali shu uslubda.
  • emitDecoratorMetadata — __metadata("design:paramtypes", [PriceList]): tur o'chadi, lekin klass qiymati yoziladi. interface → Object, import type → Function.
  • reflect-metadata — Reflect.getMetadata polyfill'i va turlari; kirish faylining birinchi qatorida import qilinadi.
  • DI konteyner metadata'ni o'qib, bog'liqliklar zanjirini o'zi quradi — new A(new B(…)) o'rniga resolve(A).

Keyingi dars: TypeScript va DOM — querySelector qaytaradigan Element | null, HTMLInputElement va hodisa turlari; vazifalar ning DOM modullari .ts ga o'tadi.

Manbalar

  • TypeScript Handbook: "Decorators" (experimentalDecorators) — typescriptlang.org/docs/handbook/decorators.html
  • TSConfig: experimentalDecorators, emitDecoratorMetadata — typescriptlang.org/tsconfig
  • reflect-metadata — github.com/microsoft/reflect-metadata (npm 0.2.2)
  • NestJS hujjatlari: "Providers", "Custom providers" — docs.nestjs.com; boshlang'ich loyiha — github.com/nestjs/typescript-starter
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
TypeScript legacy dekoratorlar: experimentalDecorators, reflect-metadata va dependency injection — IlmHamroh