IlmHamroh
JavaScript Full-stack/30-qism. NestJS8/65-dars21 daqiqa
Mundarija (30)

NestJS DI va IoC konteyner: design:paramtypes, tokenlar va bog'liqlik xatolari

Qisqacha: Bog'liqlikni tashqaridan berish (DI) — klass o'ziga kerak obyektni o'zi yaratmaydi, constructor orqali oladi. IoC konteyner — kim kimga kerakligini bilib, obyektlarni yaratadigan va ulaydigan qism. Nest bog'liqliklarni TypeScript yozib qoldirgan design:paramtypes metama'lumotidan o'qiydi (emitDecoratorMetadata + reflect-metadata). Klass turi o'zi token bo'ladi; interfeys esa ish vaqtida yo'q — metama'lumotda Object qoladi, shuning uchun unga injection token (bog'liqlik kaliti: Symbol, satr) va @Inject(TOKEN) kerak.

Bu darsda

  • Qattiq bog'lanish (tight coupling) nima uchun zararli ekanini misolda ko'rasiz.
  • IoC va DI farqini o'z so'zingiz bilan tushuntirasiz.
  • design:paramtypes ni o'zingiz o'qib, kichik DI konteyner yozasiz — dekoratorlar bilan, haqiqiy TypeScript'da.
  • Injection token turlarini (klass, Symbol, satr) va qachon qaysi biri kerakligini bilasiz.
  • Nest'ning uch xil DI xatosini haqiqiy matni bilan o'qiysiz, shu jumladan chalg'ituvchisini.

Oldin bilishingiz kerak: Provider'lar va service'lar, Legacy dekoratorlar va reflect-metadata, Symbol, express.Router va qatlamli loyiha tuzilmasi, Request lifecycle.

O'tgan darsda so'rov qaysi tartibda o'tishini ko'rdik. Bu darsda bir qadam orqaga — so'rovdan oldinga — qaytamiz: ilova ishga tushayotganda Nest controller va xizmatlarni qanday yaratadi va ularni qanday ulaydi.

1. Nega bu kerak?

Tasavvur qiling, VazifalarService omborni o'zi yaratadi:

ts
class VazifalarService {
  readonly #ombor = new PgOmbori(pgPool(process.env.DATABASE_URL));
  ol(id: number) {
    return this.#ombor.ol(id);
  }
}

Bu kod ishlaydi. Lekin uchta muammo bor:

  1. Test. Xizmatni sinash uchun haqiqiy Postgres kerak. Soxta omborni berib bo'lmaydi — u ichkarida yaratilgan.
  2. Almashtirish. SAQLASH=xotira yoki Prisma omboriga o'tish uchun xizmat kodini o'zgartirish kerak.
  3. Yashirin bog'liqlik. Constructor'ga qarab, xizmatga baza kerakligini bilib bo'lmaydi. Faylni oxirigacha o'qish kerak.

Bu — qattiq bog'lanish (tight coupling): klass o'z bog'liqligining aniq turini biladi va uni o'zi yaratadi. Teskarisi — bo'sh bog'lanish (loose coupling): klass faqat interfeysni (VazifaOmbori) biladi va uni tashqaridan oladi. 25-qismdagi vazifaXizmati(ombor) aynan shunday edi (qatlamlar darsi).

O'xshatish: devorga mahkam payvandlangan dazmol va rozetkaga ulanadigan dazmol. Birinchisini almashtirish uchun devorni buzasiz. Ikkinchisi faqat "rozetka standartini" biladi — istalgan dazmolni ulaysiz.

2. DI va IoC

2.1 Ikki tushuncha

Bog'liqlik (dependency) — klassga ishlashi uchun kerak bo'lgan boshqa obyekt. VazifalarService uchun ombor va log.

Bog'liqlikni tashqaridan berish (dependency injection, DI) — bog'liqlikni klass ichida yaratmasdan, tashqaridan (odatda constructor orqali) berish. Bu — usul. Uni qo'lda ham qilish mumkin: 25-qismda ilova.ts hamma narsani o'zi ulardi.

Boshqaruvni teskari berish (inversion of control, IoC) — obyektlarni yaratish va ulash ishini freymvork bajaradi, sizning kodingiz emas. Bu — tamoyil. Ba'zan "Gollivud tamoyili" deyiladi: "Bizga qo'ng'iroq qilmang, kerak bo'lsa o'zimiz qo'ng'iroq qilamiz".

Bu tamoyilni siz 25-qismda ham ko'rgansiz, faqat nomini aytmagandik. Express handler'ini siz chaqirmaysiz: r.get("/vazifalar", handler) deb ro'yxatdan o'tkazasiz, so'rov kelganda Express o'zi chaqiradi. Boshqaruv — "qachon chaqirish" — freymvorkda. Nest bu g'oyani bir qadam uzaytiradi: endi obyektlarni yaratish ham freymvorkda.

IoC konteyner — shu tamoyilni amalga oshiruvchi qism: u har klassning bog'liqliklarini biladi, kerakli tartibda yaratadi va tayyor nusxalarni saqlaydi. NestJS nima va nega darsida 25 qatorlik JS modelini yozgandik.

DI qo'lda (25-qism) DI + IoC konteyner (Nest)
Kim yaratadi siz, ilova.ts da konteyner
Tartib siz yozgan konteyner hisoblaydi
Bog'liqlik ro'yxati funksiya parametrlari constructor turlari (metama'lumot)
Xato qachon ishga tushishda (TypeError) yoki hech qachon ishga tushishda, aniq xabar bilan

2.2 Nest qanday "ko'radi"

Bizning JS modelimizda bog'liqliklarni qo'lda yozgandik: k.qosh(VazifalarXizmati, [Ombor]). Nest'da siz faqat constructor yozasiz. Ro'yxatni kompilyator yozadi.

tsconfig.json da emitDecoratorMetadata: true bo'lsa, TypeScript dekoratorli klass uchun JavaScript'ga qo'shimcha qator qo'shadi. Nest CLI darsida build qilingan fayldan ko'rgandik:

text
AppController = __decorate([
    Controller(),
    __metadata("design:paramtypes", [AppService])
], AppController);

__metadata("design:paramtypes", [AppService]) — "bu klass constructor'ining parametr turlari: [AppService]". reflect-metadata paketi bu yozuvni klassga bog'lab saqlaydi, Nest esa Reflect.getMetadata("design:paramtypes", Klass) bilan o'qiydi.

Ikki shart bor: emitDecoratorMetadata yoqiq bo'lishi va klassda kamida bitta dekorator bo'lishi. Provider'lar va service'lar darsidagi @Injectable() siz klass — ikkinchi shart buzilgan holat edi.

3. O'zimiz o'qib ko'ramiz

3.1 Metama'lumotda nima bor

Quyidagi blok haqiqiy TypeScript: dekoratorlar va reflect-metadata bilan. Saytda uning «Ishga tushir» tugmasi yo'q — dekorator metama'lumoti build talab qiladi. Biz uni tsc bilan JavaScript'ga o'girib, Node'da yurgizdik; natija pastda. Siz ham o'z kompyuteringizda shunday qila olasiz. Injectable va Inject — Nest'dagilarning kichik nusxasi:

ts
// @ts-options: {"experimentalDecorators": true, "emitDecoratorMetadata": true}
import "reflect-metadata";

interface Ombor { ol(): string }
const OMBOR = Symbol("OMBOR");
const TOKENLAR = "tokenlar";

// Nest'ning @Injectable va @Inject g'oyasi — kichik nusxa
const Injectable = (): ClassDecorator => () => {};
function Inject(token: symbol): ParameterDecorator {
  return (klass, _kalit, indeks) => {
    const eski: Record<number, symbol> =
      Reflect.getMetadata(TOKENLAR, klass) ?? {};
    Reflect.defineMetadata(
      TOKENLAR, { ...eski, [indeks]: token }, klass,
    );
  };
}

@Injectable()
class Soat {
  hozir() { return "09:00"; }
}

@Injectable()
class Xizmat {
  constructor(
    readonly soat: Soat,
    @Inject(OMBOR) readonly ombor: Ombor,
  ) {}
}

type Klass = new (...a: never[]) => object;
const nomi = (t: unknown) =>
  typeof t === "function" ? t.name : String(t);

function yarat(K: Klass, royxat: Map<unknown, () => unknown>) {
  const turlar: unknown[] =
    Reflect.getMetadata("design:paramtypes", K) ?? [];
  const tokenlar: Record<number, symbol> =
    Reflect.getMetadata(TOKENLAR, K) ?? {};
  const argumentlar = turlar.map((tur, i) => {
    const token = tokenlar[i] ?? tur; // @Inject ustun
    const fabrika = royxat.get(token);
    if (!fabrika) {
      throw new Error(`${K.name} (index [${String(i)}]): ` +
        `${nomi(token)} topilmadi`);
    }
    return fabrika();
  });
  return new K(...(argumentlar as never[]));
}

console.log(Reflect.getMetadata("design:paramtypes", Xizmat)
  .map(nomi));
const royxat = new Map<unknown, () => unknown>([
  [Soat, () => new Soat()],
]);
try {
  yarat(Xizmat, royxat);
} catch (e) {
  console.log((e as Error).message);
}
royxat.set(OMBOR, () => ({ ol: () => "xotira" }));
const x = yarat(Xizmat, royxat) as Xizmat;
console.log(x.soat.hozir(), x.ombor.ol());

Konsolda:

text
[ 'Soat', 'Object' ]
Xizmat (index [1]): Symbol(OMBOR) topilmadi
09:00 xotira

Uch qatorni o'qing:

  1. [ 'Soat', 'Object' ] — kompilyator ikki parametrni yozdi. Birinchisi — haqiqiy klass Soat. Ikkinchisi — Object: interfeys Ombor ish vaqtida yo'q, TypeScript uning o'rniga Object qo'ydi.
  2. index [1] — @Inject(OMBOR) metama'lumotdagi Object ni token bilan almashtirdi, lekin bunday token ro'yxatda hali yo'q. Xabar Nest'nikiga o'xshaydi — kim, nechanchi argument, qaysi token.
  3. Token ro'yxatga qo'shilgach, konteyner ikkala bog'liqlikni topdi.

Bu — Nest DI'ning butun mexanizmi, kichik shaklda. Haqiqiy Nest qo'shimcha ravishda modul chegarasini (qaysi provider qayerda ko'rinadi), singleton'ni (tayyor jadvali) va aylanma bog'liqlikni hisobga oladi.

3.2 Haqiqiy Nest metama'lumoti

Kanon xuddi shuni haqiqiy @nestjs/common dekoratorlari bilan qildi (kanon tajribasi, Vitest muhitida):

text
1) emitDecoratorMetadata — tsc/oxc constructor turlarini yozib qo'yadi:
   design:paramtypes(Xizmat) = [ 'Soat' ]
   design:paramtypes(InterfeysliXizmat) = [ 'Object' ]
   self:paramtypes(TokenliXizmat) = [{"index":1,"param":"YOQ"},{"index":0,"param":"Symbol(OMBOR)"}]

self:paramtypes — Nest'ning o'z kaliti: @Inject(...) shu yerga "index → token" juftini yozadi. Bizning modeldagi TOKENLAR aynan uning o'rni. Nest bog'liqlikni qidirganda self:paramtypes dagi tokenni design:paramtypes dagi turdan ustun qo'yadi. oxc — Vitest ishlatadigan Vite kompilyatori; u ham, tsc ham bu metama'lumotni yozadi.

Tekshirib ko'ring: constructor(private readonly log: Log) — Log bu import type { Log } from "./log.ts" bilan olingan pino logger turi. design:paramtypes da nima bo'ladi?

Javob

Object. Log — TypeScript turi, ish vaqtida mavjud emas; import type esa kompilyatsiyadan keyin butunlay o'chadi. Shuning uchun vazifalar-api da @Inject(LOG) yoziladi. Klass bo'lganda ham import type bilan olinsa, xuddi shu muammo chiqadi — buni Nest xabari ham eslatadi (keyingi bo'lim).

3.3 Oddiy turlar: string → String

Interfeys Object ga aylanishini ko'rdik. Oddiy turlar-chi — string, number, birlashma ("yangi" | "bajarildi"), massiv? Kompilyator ular uchun ham nimadir yozadi. Quyidagi blok shuni ko'rsatadi:

ts
// @ts-options: {"experimentalDecorators": true, "emitDecoratorMetadata": true}
import "reflect-metadata";

const Injectable = (): ClassDecorator => () => {};
type Holat = "yangi" | "bajarildi";

@Injectable()
class Hisobot {
  constructor(
    readonly tz: string,
    readonly limit: number,
    readonly holat: Holat,
    readonly kun: Date,
    readonly teglar: string[],
  ) {}
}

const turlar: unknown[] =
  Reflect.getMetadata("design:paramtypes", Hisobot);
console.log(turlar.map((t) => (t as { name: string }).name));

Konsolda:

text
[ 'String', 'Number', 'String', 'Date', 'Array' ]

Nima bo'ldi? Kompilyator har turni JavaScript'da mavjud eng yaqin konstruktor bilan almashtirdi. string — String, number — Number. Holat birlashmasi — ham String, chunki uning hamma qiymati satr. string[] — shunchaki Array: ichidagi tur yo'qoldi.

Bu Nest uchun nimani anglatadi? String — klass, demak Nest uni token deb qidiradi. Demo loyihada constructor(readonly tz: string) li xizmatni ishga tushirdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0). Xabarning boshi:

text
Nest can't resolve dependencies of the HisobotService (?). Please make sure that the argument String at index [0] is available in the HisobotModule module.

Bu safar argumentning nomi bor — String. Lekin hech kim String ni provider qilib ro'yxatdan o'tkazmaydi. Xabar "String provider'ini qo'shing" deyayotgandek tuyuladi, aslida esa sabab boshqa: satr sozlamani constructor'ga tur bilan so'rab bo'lmaydi. Yechim interfeysdagidek — token: @Inject(ILOVA_SOZLAMALARI) sozlamalar: IlovaSozlamalari. vazifalar-api dagi tz aynan shu yo'l bilan keladi.

Tekshirib ko'ring: constructor(readonly ombor: VazifaOmbori, readonly limit: number). VazifaOmbori — interfeys. Nest xabarida argumentlar qanday ko'rinadi va qaysi biri birinchi bo'lib xato beradi?

Javob

Demo loyihada tekshirdik. Xabar: dependencies of the A (?, Number) va the argument at index [0] — nomsiz. Interfeys Object ga aylangan, Nest uni "noma'lum" deydi. Nest argumentlarni chapdan o'ngga yechadi va birinchi topilmaganida to'xtaydi, shuning uchun xabar index [0] haqida. Birinchisini @Inject(VAZIFA_OMBORI) bilan tuzatdik — keyingi ishga tushishda xabar argument Number at index [1] ga o'tdi. Ikkalasini bir yo'la tuzating: ikkala parametrga ham token.

4. Injection token

4.1 Uch xil token

Injection token (bog'liqlik kaliti) — konteynerdagi provider'ning nomi. Konteyner "nima kerak?" degan savolga shu kalit bo'yicha javob qidiradi.

Token Misol Qachon
klass VazifalarService provider — klass; tur va token bir xil
Symbol VAZIFA_OMBORI interfeys, oddiy qiymat, sozlama
satr "OMBOR" kichik loyiha, tez sinov

Klass tokeni eng qulay — hech narsa yozmaysiz, Nest turdan oladi. Interfeys va oddiy qiymat uchun esa token o'ylab topiladi.

Tokenni do'kondagi tovar shtrix-kodiga o'xshatish mumkin. Sotuvchi tovarni ko'rinishiga qarab emas, shtrix-kodi bo'yicha topadi. Bir xil ko'rinishdagi ikki tovarning kodi har xil bo'lishi mumkin — tizim ularni adashtirmaydi. Symbol esa har chaqiruvda yangi shtrix-kod beradi.

4.2 Nega Symbol, satr emas

Satr token (@Inject("CONFIG")) ham ishlaydi. Lekin ikki kutubxona bir xil "CONFIG" ni ishlatsa, ular bir-birining provider'ini ustidan yozadi — xato jim chiqadi. Symbol("CONFIG") esa har chaqiruvda yangi, takrorlanmas qiymat (Symbol). Uni faqat eksport qilingan o'zgaruvchi orqali olish mumkin — tasodifiy to'qnashuv yo'q.

Symbol("…") ichidagi satr — faqat tavsif. U xato xabarlarida ko'rinadi: Provider'lar darsidagi Symbol(VAZIFA_OMBORI) shu. Tavsifni token nomiga teng yozing — xabar o'qilishi oson bo'ladi.

vazifalar-api da hamma token bitta faylda — tokenlar.ts. Bitta joy: qaysi tokenlar borligini ko'rish uchun bitta faylni ochish yetadi.

Diqqat: Token qiymati bilan uning turi alohida yashaydi. @Inject(LOG) private readonly log: Log da Nest faqat LOG ga qaraydi. Agar LOG ostida boshqa narsa ro'yxatdan o'tgan bo'lsa (masalan, xato bilan sozlama obyekti), TypeScript buni ushlamaydi. Token va tur juftligini bitta faylda, yonma-yon izoh bilan saqlang.

5. Nest'ning DI xatolari

Kanon uch xil tipik xatoni ataylab chiqardi. Xabarlar qatorlari bo'yicha qisqartirilgan (Nest bo'sh qatorlarni ham beradi).

5.1 Provider ro'yxatda yo'q

Xizmat constructor'ida Soat bor, lekin modulning providers ida Soat yo'q. Xabarning birinchi qatoridagi uch narsaga qarang: qavs ichidagi (?), argument nomi va modul nomi:

text
2) Soat provider sifatida ro'yxatda yo'q: XATO
   Nest can't resolve dependencies of the Xizmat (?). Please make sure that the argument Soat at index [0] is available in the ProviderYoq module.
   Potential solutions:
   - Is ProviderYoq a valid NestJS module?
   - If Soat is a provider, is it part of the current ProviderYoq?
   - If Soat is exported from a separate @Module, is that module imported within ProviderYoq?
   @Module({
   imports: [ /* the Module containing Soat */ ]
   })
   For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors

Modullar va Provider'lar darslarida ko'rgan xabar. Bu safar uni "konteyner tili" bilan o'qiymiz: konteyner design:paramtypes dan Soat ni oldi, uni joriy modul ko'rinishida qidirdi va topmadi. Soat klass bo'lgani uchun xabarda nomi aniq.

5.2 Interfeys token'siz — chalg'ituvchi xabar

InterfeysliXizmat ning constructor'ida readonly ombor: Ombor — interfeys, @Inject siz. Bu xabar uzun, lekin uning ikki joyi muhim: birinchi qatordagi argument nomi va «Potential solutions» ro'yxati:

text
3) interfeys turi token'siz (Object): XATO
   Nest can't resolve dependencies of the InterfeysliXizmat (?). Please make sure that the argument at index [0] is available in the current module.
   Potential solutions:
   - The dependency at index [0] appears to be undefined at runtime
   - This commonly occurs when using 'import type' instead of 'import' for injectable classes
   - Check your imports and change:
   ❌ import type { SomeService } from './some.service';
   ✅ import { SomeService } from './some.service';
   - Ensure the imported class is decorated with @Injectable() or is a valid provider
   - If using dynamic imports, ensure the class is available at runtime, not just for type checking
   For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors

Tarjimasi: "0-indeksdagi bog'liqlik ish vaqtida undefined ko'rinadi. Bu ko'pincha injectable klasslar uchun import o'rniga import type ishlatilganda yuz beradi."

Birinchi qatorga qarang: the argument at index [0] — argument nomi yo'q (Soat yoki Symbol(OMBOR) kabi). Maslahatlar esa butunlay boshqa narsa haqida: import type va @Injectable(). Bizning holatda ikkalasi ham to'g'ri — sabab interfeys. Metama'lumotda Object turibdi, Nest esa uni "noma'lum" deb hisobladi.

Shuning uchun bu xabar chalg'itadi: siz importlarni tekshirib, hech narsa topmaysiz. O'qish qoidasi: xabarda argument nomi yo'q bo'lsa — avval constructor'dagi o'sha indeksga qarang. U interfeys yoki type bo'lsa — @Inject(TOKEN) qo'shing. Klass bo'lsa — import type emasligini tekshiring.

5.3 Token bor, provider yo'q

text
4) @Inject(OMBOR) bor, lekin OMBOR provider'i yo'q: XATO
   Nest can't resolve dependencies of the TokenliXizmat (?, YOQ). Please make sure that the argument Symbol(OMBOR) at index [0] is available in the TokenProviderYoq module.

Qolgan maslahatlar «Provider ro'yxatda yo'q» bo'limidagidek. Endi argument nomi — Symbol(OMBOR). Demak token bor, lekin modulda bunday provider yo'q. (?, YOQ) — ikkinchi argument @Optional() @Inject("YOQ"); u ham yo'q, lekin ixtiyoriy bo'lgani uchun xato faqat birinchisida. Provider'lar darsidagi Symbol(VAZIFA_OMBORI) xatosi aynan shu tur edi.

flowchart TB
  A["constructor parametri i"] --> B{"self:paramtypes da token bormi?"}
  B -- ha --> C["token bo'yicha qidir"]
  B -- yo'q --> D{"design:paramtypes turi"}
  D -- klass --> E["klass bo'yicha qidir"]
  D -- "Object / undefined" --> F["xato: argument nomsiz"]
  C --> G{"modulda bormi?"}
  E --> G
  G -- ha --> H["nusxa beriladi"]
  G -- yo'q --> I["xato: nomi bilan"]

Diagramma xatolarning qayerdan chiqishini ko'rsatadi. Pastdagi «xato: nomi bilan» — provider ro'yxatda yo'q holatlar (klass ham, token ham). «Xato: argument nomsiz» — interfeys tokensiz qolgan holat.

Tekshirib ko'ring: Nest can't resolve dependencies of the EksportService (Symbol(VAZIFA_OMBORI), ?) — qaysi argument muammo va qayerdan izlaysiz?

Javob

Ikkinchi (index [1]) argument. Birinchisi — VAZIFA_OMBORI — topildi. EksportService constructor'ida ikkinchi parametr — @Inject(LOG) log. Demak LOG provider'i ko'rinmayapti: global modul ulanmagan yoki exports da LOG yo'q. Xabarning qolgan qismi (argument … at index [1] is available in the EksportModule) aniq tokenni aytadi.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
the argument at index [N] (nomsiz) interfeys/tur tokensiz yoki import type klass @Inject(TOKEN) yoki oddiy import
argument Symbol(X) at index [N] token bor, provider ro'yxatda yo'q { provide: X, … } qo'shing
Xizmat ishlaydi, lekin noto'g'ri obyekt keladi ikki joyda bir xil satr token Symbol token
Testda soxta ombor berib bo'lmaydi klass bog'liqligini o'zi new qiladi constructor orqali oling
design:paramtypes umuman yo'q emitDecoratorMetadata o'chiq yoki dekorator yo'q tsconfig va @Injectable()
argument String at index [N] constructor'da string (yoki number) turi sozlama uchun token va @Inject

7. Mashqlar

Mashqlar kurs/mashqlar/30/08-di-konteyner/ da.

1-mashq (oson): metama'lumotni toping

constructor(soat: Soat, @Inject(LOG) log: Log, sozlama: { tz: string }) uchun:

  • design:paramtypes dagi birinchi element —
  • Ikkinchi element (Log — pino turi) —
  • Uchinchi element (obyekt turi) —
  • Qaysi argument Nest'da ishlamaydi va xabarda nomsiz chiqadi? index []
Yechim
  • Soat — klass, ish vaqtida mavjud.
  • Object — tur ish vaqtida yo'q; lekin @Inject(LOG) uni token bilan almashtiradi, shuning uchun bu argument ishlaydi.
  • Object — obyekt turi; token yo'q.
  • 2 — uchinchi argument: tokensiz Object. Yechim — @Inject(ILOVA_SOZLAMALARI), vazifalar-api dagidek.

2-mashq (o'rta): qattiq bog'lanishni yechish

Quyidagi klass bog'liqligini o'zi yaratadi:

js
class Bildirishnoma {
  #sms = new HaqiqiySms(); // tarmoqqa yuboradi
  yubor(tel, matn) {
    return this.#sms.jonat(tel, `[IlmHamroh] ${matn}`);
  }
}

Uni qayta yozing: SMS jo'natuvchi constructor'dan kelsin. Soxta jo'natuvchi ({ jonat: (tel, m) => ... }) yuborilgan xabarlarni massivga yozsin. Ikki xabar yuborib, massivni chiqaring. Ishora: klass sintaksisi, push.

Yechim
js
class Bildirishnoma {
  constructor(sms) {
    this.sms = sms;
  }
  yubor(tel, matn) {
    return this.sms.jonat(tel, `[IlmHamroh] ${matn}`);
  }
}

const yuborilgan = [];
const soxtaSms = {
  jonat: (tel, m) => {
    yuborilgan.push(`${tel}: ${m}`);
    return true;
  },
};
const b = new Bildirishnoma(soxtaSms);
b.yubor("+998 90 000 00 00", "Vazifa bajarildi");
b.yubor("+998 90 000 00 01", "Yangi izoh");
console.log(yuborilgan);

Konsolda:

text
[
  '+998 90 000 00 00: [IlmHamroh] Vazifa bajarildi',
  '+998 90 000 00 01: [IlmHamroh] Yangi izoh'
]

Endi Bildirishnoma qaysi SMS provayderi ishlatilishini bilmaydi — faqat jonat metodi borligini. Testda soxta, prodakshnda haqiqiy (29-qismdagi Eskiz yoki PlayMobile moslagichi). Nest'da sms parametri oldiga @Inject(SMS) qo'yilardi — SMS jo'natuvchi interfeys bo'lgani uchun.

3-mashq (qiyin): satr token to'qnashuvi

Kichik konteyner modelida (Map: token → qiymat) ikki "kutubxona" provider qo'shadi. Birinchi kutubxona "CONFIG" satr tokeni ostida { tz: "Asia/Tashkent" } ni, ikkinchisi xuddi shu satr ostida { dsn: "…" } ni ro'yxatdan o'tkazadi. Birinchi kutubxonaning xizmati "CONFIG" ni olib, tz ni chiqarsin — nima chiqadi? Keyin ikkala kutubxonani Symbol("CONFIG") bilan qayta yozing va natijani solishtiring. Ishora: Map.set bir xil kalitni ustidan yozadi (Map); har Symbol(...) — yangi kalit.

Yechim
js
function konteyner() {
  const royxat = new Map();
  return {
    qosh: (token, qiymat) => royxat.set(token, qiymat),
    ol: (token) => royxat.get(token),
    soni: () => royxat.size,
  };
}

const k1 = konteyner();
k1.qosh("CONFIG", { tz: "Asia/Tashkent" }); // 1-kutubxona
k1.qosh("CONFIG", { dsn: "https://example.com" }); // 2-kutubxona
console.log("satr:", k1.ol("CONFIG").tz, k1.soni());

const K1_CONFIG = Symbol("CONFIG");
const K2_CONFIG = Symbol("CONFIG");
const k2 = konteyner();
k2.qosh(K1_CONFIG, { tz: "Asia/Tashkent" });
k2.qosh(K2_CONFIG, { dsn: "https://example.com" });
console.log("Symbol:", k2.ol(K1_CONFIG).tz, k2.soni());
console.log(K1_CONFIG === K2_CONFIG, String(K1_CONFIG));

Konsolda:

text
satr: undefined 1
Symbol: Asia/Tashkent 2
false Symbol(CONFIG)

Satr tokenda ikkinchi kutubxona birinchisining sozlamasini jim ustidan yozdi: konteynerda bitta yozuv qoldi va tz — undefined. Hech qanday xato yo'q. Symbol bilan ikkala yozuv saqlandi, garchi tavsiflari bir xil bo'lsa ham (false — ular har xil qiymat). Haqiqiy Nest'da ham bitta modul ichida bir xil token ostidagi ikki provider'dan oxirgisi qoladi.

8. Loyiha qadami: CONFIG tokeni

8.1 Reja

Bu dars — holatning birinchi qismi. Keyingi darslarda ombor Nest ichida, config bo'yicha yaratiladi (hozir u main.ts da, Nest'dan oldin). Buning uchun fabrikaga to'liq config kerak bo'ladi. Bu darsda config uchun token yaratamiz va uni konteynerga beramiz.

8.2 tokenlar.ts

ts
// tokenlar.ts — DI tokenlari: interfeys (VazifaOmbori) va oddiy
// qiymatlar (log, sozlamalar) uchun. TS interfeysi ish vaqtida YO'Q —
// Nest uni constructor turidan taniy olmaydi, shuning uchun
// Symbol token + @Inject(TOKEN)
export const VAZIFA_OMBORI = Symbol("VAZIFA_OMBORI");
export const LOG = Symbol("LOG");
export const XABARCHI = Symbol("XABARCHI");
export const ILOVA_SOZLAMALARI = Symbol("ILOVA_SOZLAMALARI");
/** to'liq, tekshirilgan Config (keyinroq @nestjs/config bilan
 * almashadi) */
export const CONFIG = Symbol("CONFIG");

/** Controller'larga kerakli kichik sozlamalar */
export type IlovaSozlamalari = { tz: string };

Ikki o'zgarish. Birinchisi: CONFIG qo'shildi — configYukla qaytargan to'liq, tekshirilgan obyekt uchun (Env validatsiyasi va tipli config obyekti). Ikkinchisi: IDEMPOTENTLIK_OMBORI o'chdi: Controller'lar darsida zaxira uchun yozilgan edi, lekin hech qayerda ishlatilmadi (idempotentlik ombori hozircha middleware'ga to'g'ridan beriladi). Ishlatilmagan token — keyingi o'quvchi uchun yolg'on iz: "bu qayerda provider?". Eksport qilingan bo'lgani uchun linter uni ushlamaydi — qo'lda tozalaymiz.

8.3 bogliqliklar.module.ts

ts
// bogliqliklar.module.ts — tashqarida yaratilgan obyektlar (ombor,
// log, xabarchi, config) DI konteyneriga beriladi: useValue
// provider'lar. @Global — har modul import qilmasdan ishlatadi;
// forRoot(...) — dinamik modul (keyinroq batafsil)
import { Global, Module } from "@nestjs/common";
import type { DynamicModule, Provider } from "@nestjs/common";
import {
  CONFIG, ILOVA_SOZLAMALARI, LOG, VAZIFA_OMBORI, XABARCHI,
} from "./tokenlar.ts";
import type { IlovaSozlamalari } from "./tokenlar.ts";
import type { Config } from "./config.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";
import type { Log } from "./umumiy/log.ts";
import type { Xabarchi } from "./umumiy/webhook.ts";

export type TashqiBogliqliklar = {
  ombor: VazifaOmbori;
  log: Log;
  xabarchi?: Xabarchi | undefined;
  sozlamalar: IlovaSozlamalari;
  config?: Config | undefined;
};

@Global()
@Module({})
export class BogliqliklarModule {
  static forRoot(
    { ombor, log, xabarchi, sozlamalar, config }: TashqiBogliqliklar,
  ): DynamicModule {
    const providers: Provider[] = [
      { provide: VAZIFA_OMBORI, useValue: ombor },
      { provide: LOG, useValue: log },
      // xabarchi ixtiyoriy: yo'q bo'lsa null (xizmat @Optional bilan
      // oladi)
      { provide: XABARCHI, useValue: xabarchi ?? null },
      { provide: ILOVA_SOZLAMALARI, useValue: sozlamalar },
      ...(config === undefined
        ? []
        : [{ provide: CONFIG, useValue: config }]),
    ];
    return {
      module: BogliqliklarModule,
      providers,
      exports: providers.map(
        (p) => (p as { provide: symbol }).provide,
      ),
    };
  }
}

config ixtiyoriy: testlar uni bermaydi (ularga faqat ombor kerak), shuning uchun CONFIG provider'i faqat config bo'lsa qo'shiladi. ...(shart ? [] : [x]) — massivga shartli element qo'shishning keng tarqalgan usuli (spread).

Bu shartli element tur xulosasini buzadi: TypeScript providers ni endi bir xil shakldagi obyektlar massivi deb ololmaydi. Shuning uchun Provider[] — Nest'ning provider turi — aniq yozildi. Provider ichida provide maydoni yo'q turlar ham bor (oddiy klass ham provider), shuning uchun exports da (p as { provide: symbol }) — "bu massivda faqat provide li obyektlar bor, men bilaman" degan tasdiq. Tasdiq (as) xavfli vosita (Type assertion) — u bu yerda adolatli, chunki massivni bir necha qator yuqorida o'zimiz yozdik.

8.4 ilova.ts va main.ts

ilova.ts — Bogliqliklar turiga yangi maydon va AppModule.forRoot ga uzatish:

diff
 import type { VazifaOmbori } from "./vazifalar/ombor.ts";
+import type { Config } from "./config.ts";

 export type Bogliqliklar = {
   ombor: VazifaOmbori;
+  /** to'liq config (main.ts) — DI ichida CONFIG tokeni */
+  config?: Config;
diff
 export async function ilovaYarat({
   ombor,
+  config,
   corsOriginlar = [],
diff
-    AppModule.forRoot({ ombor, log, xabarchi, sozlamalar: { tz } }),
+    AppModule.forRoot({
+      ombor, config, log, xabarchi, sozlamalar: { tz },
+    }),

main.ts — bogliqliklar ga bitta qator: config, (ombor: saqlash.ombor, dan keyin). AppModule.forRoot o'zgarmaydi: u TashqiBogliqliklar ni to'liq BogliqliklarModule.forRoot ga uzatadi.

8.5 Tekshiruv

bash
npm run tip
npm run lint
npm test

Biz kanon nusxasida yurgizdik (bazasiz): tip 0, lint 0, Vitest 71 o'tdi, 5 fayl SKIP (Postgres va Redis kerak). Xulq o'zgarmadi: CONFIG hali hech kim tomonidan so'ralmaydi — u keyingi darsdagi fabrika uchun tayyorlandi. Hali commit qilmang — bu holatning commit'i Reflector va DiscoveryService darsida.

9. Real ishda

  • Test yozishda DI'ning foydasi darrov ko'rinadi: Test.createTestingModule({ providers: [VazifalarService, { provide: VAZIFA_OMBORI, useValue: soxta }] }) — haqiqiy baza kerak emas (NestJS unit testlari).
  • Katta jamoalarda tokenlar bitta faylda yoki modul bilan yonma-yon (ombor.tokens.ts) saqlanadi. Satr tokenlar — kod review'da odatda rad etiladi.
  • "Chalg'ituvchi xabar" (argument at index [0], nomsiz) — Nest forumlarida eng ko'p so'raladigan xatolardan biri. Uning sababi deyarli doim interfeys yoki import type.
  • Sozlamalar: Nest ekotizimida satr va son sozlamalari constructor'ga string turi bilan emas, klass orqali keladi: ConfigService — oddiy klass, u token bo'la oladi va undan config.get("PORT") bilan o'qiladi. Bu yo'lni @nestjs/config darsida ko'ramiz. Ikkala usulda ham g'oya bitta: ish vaqtida mavjud bo'lmagan tur — token emas.
  • Intervyu: "DI va IoC farqi nima?", "Nest interfeys turidagi bog'liqlikni qanday topadi?", "emitDecoratorMetadata nima qiladi?", "Nega Symbol token?".

Xulosa

  • Qattiq bog'lanish testni va almashtirishni qiyinlashtiradi; DI — bog'liqlikni tashqaridan berish, IoC — buni freymvorkka topshirish.
  • Nest bog'liqliklarni design:paramtypes dan o'qiydi; uni emitDecoratorMetadata va dekorator yozadi.
  • Interfeys va tur ish vaqtida Object — ular uchun injection token va @Inject(TOKEN); self:paramtypes tokenni turdan ustun qo'yadi.
  • string va number metama'lumotda String va Number bo'ladi — Nest ularni provider deb qidiradi; sozlama uchun ham token kerak.
  • Symbol token to'qnashmaydi; satr token jim ustidan yozilishi mumkin.
  • Xabarda argument nomi bo'lmasa — interfeys yoki import type; nomi bo'lsa — provider ro'yxatda yo'q.
  • vazifalar-api ga CONFIG tokeni qo'shildi — keyingi darsdagi fabrika uchun.

Keyingi dars: Custom provider'lar — useValue, useClass, useFactory + inject, useExisting va async fabrika; omborni Nest ichida, config bo'yicha yaratadigan provider'larni yozamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS DI va IoC konteyner: design:paramtypes, tokenlar va bog'liqlik xatolari — IlmHamroh