IlmHamroh
JavaScript Full-stack/15-qism. TypeScript12/47-dars20 daqiqa
Mundarija (29)

TypeScript discriminated union va to'liqlik tekshiruvi: kind, switch va never

Qisqacha: Discriminated union (ajratuvchi maydonli union) — har varianti umumiy literal maydonga ega obyektlar union'i: { kind: "tayyor"; readyAt: string } | { kind: "bekor"; reason: string }. Ajratuvchini (kind) tekshirsangiz, TypeScript qaysi variant ekanini va unda qaysi maydonlar borligini biladi. switch ning default ida qiymatni never ga bersangiz — to'liqlik tekshiruvi: yangi variant qo'shilib, uni unutsangiz, tsc xato beradi.

Bu darsda

  • "Imkonsiz holatlar"ni turlar bilan yo'qotasiz: ixtiyoriy maydonlar to'plami o'rniga variantlar.
  • Ajratuvchi (discriminant) maydon bilan union'ni if va switch da toraytirasiz.
  • never bilan "hamma variant ko'rib chiqildi" tekshiruvini yozasiz va assertNever yordamchisini bilasiz.
  • Holat mashinasini (state machine) discriminated union bilan modellashtirasiz.
  • vazifalar ga literal union'lar va birinchi ishlaydigan .ts modulni qo'shasiz.

Oldin bilishingiz kerak: Narrowing: turni toraytirish, Union va literal turlar, switch.

1. Nega bu kerak?

«Bahor» buyurtmasi bir necha holatdan o'tadi: yangi → tayyorlanmoqda → tayyor → berildi. Ba'zan — bekor qilinadi. Sardor birinchi variantni bitta obyekt bilan yozdi:

ts
type Order = {
  status: "yangi" | "tayyorlanmoqda" | "tayyor" | "bekor";
  cook?: string; // tayyorlanmoqda bo'lsa
  readyAt?: string; // tayyor bo'lsa
  reason?: string; // bekor bo'lsa
};

const strange: Order = { status: "bekor", readyAt: "19:40" };
console.log(strange.reason ?? "sabab yo'q"); // sabab yo'q

tsc — xatosiz. Lekin bu buyurtma ma'nosiz: "bekor qilingan, sababi yo'q, lekin 19:40 da tayyor bo'lgan". Uchta ixtiyoriy maydon — sakkizta kombinatsiya, ularning ko'pi imkonsiz holat. Har joyda order.reason ?? …, order.cook! — va har biri unutilishi mumkin.

Muammo shundaki, tur "qaysi maydon qaysi holatda bo'ladi" degan bilimni saqlamaydi. Bu bilim faqat izohlarda. Bugun uni turga ko'chiramiz.

2. Discriminated union

2.1 Har holat — alohida variant

Har holatni o'z maydonlari bilan alohida obyekt turi qilib yozamiz:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

const a: OrderState = { kind: "tayyorlanmoqda", cook: "Jasur aka" };
const b: OrderState = { kind: "bekor", reason: "mehmon ketdi" };
console.log(a.kind, b.kind); // tayyorlanmoqda bekor

Birinchi qator boshidagi | — ixtiyoriy, faqat chiroy uchun: har variant alohida qatorda turadi.

Har variantda kind bor va uning turi — literal: "yangi", "tayyor". Bu umumiy maydon ajratuvchi (discriminant) deyiladi: uning qiymati qaysi variant ekanini bir ma'noli aytadi. Butun tur esa — discriminated union (ajratuvchi maydonli union). Maydon nomi istalgancha bo'lishi mumkin — kind, type, status, tur. Muhimi: hamma variantda bor va har birida boshqa literal.

Kundalik o'xshatish — pochta jo'natmasi: yorliqdagi "XAT", "POSILKA", "BANDEROL" so'zi qutini ochmasdan ichida nima borligini aytadi. Posilkada og'irlik bor, xatda — yo'q.

2.2 Imkonsiz holatlar endi yozilmaydi

«Nega bu kerak?» dagi ma'nosiz buyurtmalar:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

const noReason: OrderState = { kind: "bekor" };
const mixed: OrderState = { kind: "tayyor", reason: "?" };

Kompilyator:

text
TS2322: Type '{ kind: "bekor"; }' is not assignable to type 'OrderState'.
  Property 'reason' is missing in type '{ kind: "bekor"; }' but required in type '{ kind: "bekor"; reason: string; }'.
TS2353: Object literal may only specify known properties, and 'reason' does not exist in type '{ kind: "tayyor"; readyAt: string; }'.

Diqqat qiling: TypeScript kind ga qarab aynan qaysi variant bilan solishtirishni tanladi. Xabarlar butun OrderState haqida emas, "bekor" va "tayyor" variantlari haqida. "Bekor — sababsiz" ham, "tayyor — sabab bilan" ham endi yozib bo'lmaydi.

2.3 kind ni tekshirmasdan — faqat umumiy maydon

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

function why(state: OrderState) {
  return state.reason;
}

Kompilyator:

text
TS2339: Property 'reason' does not exist on type 'OrderState'.
  Property 'reason' does not exist on type '{ kind: "yangi"; }'.

Union darsidagi qoida: union bilan faqat umumiy narsa — bu yerda faqat kind. Ikkinchi qator birinchi "aybdor" variantni ko'rsatdi: yangi buyurtmada sabab yo'q.

2.4 kind bo'yicha toraytirish

O'tgan darsdagi tenglik bilan toraytirish:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

function why(state: OrderState): string {
  if (state.kind === "bekor") {
    return state.reason;
  }
  return "bekor qilinmagan";
}

const cancelled: OrderState = { kind: "bekor", reason: "ketdi" };
console.log(why(cancelled)); // ketdi

state.kind === "bekor" — union'dan faqat { kind: "bekor"; reason: string } qoldi, reason endi bor. in bilan ham bo'lardi ("reason in state"), lekin kind aniqroq: u har variantda bor va qiymatlari takrorlanmaydi.

3. switch va to'liqlik tekshiruvi

3.1 switch — har variantga bitta case

Variantlar ko'p bo'lsa, switch o'qishga qulayroq. Har case ichida tur o'sha variantga toraygan:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

function label(state: OrderState): string {
  switch (state.kind) {
    case "yangi":
      return "Navbatda";
    case "tayyorlanmoqda":
      return `Oshpaz: ${state.cook}`;
    case "tayyor":
      return `Tayyor: ${state.readyAt}`;
    case "bekor":
      return `Bekor: ${state.reason}`;
  }
}

const ready: OrderState = { kind: "tayyor", readyAt: "19:40" };
console.log(label(ready)); // Tayyor: 19:40

default yo'q, lekin tsc jim — natija turi string va hamma yo'l return bilan tugaydi. TypeScript to'rt case dan keyin state ni never deb biladi: "boshqa variant qolmadi".

3.2 Bitta case unutilsa

Jasur aka "berildi" holatini qo'shishni so'radi. Sardor uni turga qo'shdi, lekin label ni unutdi. Natija turi yozilgan bo'lsa, buni darhol bilamiz:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

function label(state: OrderState): string {
  switch (state.kind) {
    case "yangi":
      return "Navbatda";
    case "tayyor":
      return `Tayyor: ${state.readyAt}`;
  }
}

Kompilyator:

text
TS2366: Function lacks ending return statement and return type does not include 'undefined'.

Bu xabar void darsida ko'rgan TS2366. TypeScript hisobladi: "bekor" holatida funksiya oxiriga yetib, hech narsa qaytarmaydi. Lekin xabar qaysi variant unutilganini aytmaydi. Va natija turi yozilmasa yoki funksiya hech narsa qaytarmasa (void), umuman jim qoladi.

3.3 never bilan aniq tekshiruv

Ishonchli usul — default da qiymatni never turli o'zgaruvchiga berish:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "bekor"; reason: string };

function label(state: OrderState): string {
  switch (state.kind) {
    case "yangi":
      return "Navbatda";
    case "tayyor":
      return `Tayyor: ${state.readyAt}`;
    default: {
      const unhandled: never = state;
      return unhandled;
    }
  }
}

Kompilyator:

text
TS2322: Type '{ kind: "bekor"; reason: string; }' is not assignable to type 'never'.

Endi xabar aniq: "{ kind: "bekor"; … } ni never ga berib bo'lmaydi" — ya'ni default ga hali "bekor" varianti yetib kelyapti. Hamma case yozilsa, default da state — never, va never ni never ga berish mumkin: xato yo'qoladi.

Bu — to'liqlik tekshiruvi (exhaustiveness check). G'oya o'tgan darslarda aytilgan edi: never ga hech qanday qiymat sig'maydi. Agar default ga biror qiymat yetib kelsa — bu ko'rib chiqilmagan variant.

flowchart TD
  A["state: yangi | tayyor | bekor"] --> B{"case yangi"}
  B -- "yo'q" --> C{"case tayyor"}
  C -- "yo'q" --> D["default: state — bekor"]
  D --> E{"const x: never = state"}
  E --> F["TS2322: bekor ≠ never<br/>case yozilmagan!"]
  B -- "ha" --> R1["return"]
  C -- "ha" --> R2["return"]

3.4 assertNever yordamchisi

Har switch da const …: never yozish o'rniga, ko'p loyihalarda kichik yordamchi bor:

ts
function assertNever(value: never): never {
  throw new Error(`Kutilmagan holat: ${JSON.stringify(value)}`);
}

type Size = "yarim" | "to'liq";

function portion(size: Size): number {
  switch (size) {
    case "yarim":
      return 0.6;
    case "to'liq":
      return 1;
    default:
      return assertNever(size);
  }
}

console.log(portion("yarim")); // 0.6

U ikki ish qiladi. Kompilyatsiyada: argument never bo'lishi shart — unutilgan variant TS2345: Argument of type '…' is not assignable to parameter of type 'never'. beradi. Ishlash paytida: agar qandaydir yo'l bilan begona qiymat kelib qolsa, jim o'tib ketmasdan aniq xabar bilan xato tashlaydi. Oddiy literal union'larda ham (Size) ishlaydi.

Tekshirib ko'ring: type Payment = "naqd" | "karta" | "click"; va switch da faqat "naqd" va "karta" uchun case bor, default: return assertNever(p);. tsc nima deydi?

Javob

TS2345: Argument of type '"click"' is not assignable to parameter of type 'never'. — default ga "click" yetib keldi. Xabar unutilgan variantni nomi bilan aytdi: unga case "click": qo'shish kerak.

4. Holat mashinasi

4.1 Holatlar va o'tishlar

Buyurtma holatlari tasodifiy almashmaydi: "berildi" dan "yangi" ga qaytib bo'lmaydi, "bekor" — oxirgi holat. Bunday tizim holat mashinasi (state machine) deyiladi: chekli holatlar va ular orasidagi ruxsat etilgan o'tishlar.

stateDiagram-v2
  [*] --> yangi
  yangi --> tayyorlanmoqda: oshpaz oldi
  yangi --> bekor: mehmon ketdi
  tayyorlanmoqda --> tayyor: pishdi
  tayyor --> berildi: stolga
  berildi --> [*]
  bekor --> [*]

Discriminated union holatlarni yaxshi ifodalaydi: har holat o'z ma'lumoti bilan (cook, readyAt, reason). O'tishlar esa funksiya bo'ladi: "joriy holat + hodisa → yangi holat".

4.2 O'tish funksiyasi

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string };

function startCooking(state: OrderState, cook: string): OrderState {
  if (state.kind !== "yangi") {
    throw new Error(`"${state.kind}" holatida boshlab bo'lmaydi`);
  }
  return { kind: "tayyorlanmoqda", cook };
}

function finish(state: OrderState, time: string): OrderState {
  if (state.kind !== "tayyorlanmoqda") {
    throw new Error(`"${state.kind}" holatida tugatib bo'lmaydi`);
  }
  return { kind: "tayyor", readyAt: time };
}

const cooking = startCooking({ kind: "yangi" }, "Jasur aka");
const ready = finish(cooking, "19:40");
console.log(ready); // { kind: 'tayyor', readyAt: '19:40' }

Har funksiya yangi holatni yangi obyekt sifatida qaytaradi — eski holat o'zgarmaydi. Noto'g'ri o'tish (finish({ kind: "yangi" }, …)) ishlash paytida xato beradi. Turlar esa har holat to'g'ri ma'lumot bilan yaratilishini kafolatlaydi: { kind: "tayyor" } ni readyAt siz qaytarib bo'lmaydi.

5. TypeScript nimani ushlamaydi

To'liqlik tekshiruvi — kompilyatsiya paytida. Tashqaridan kelgan holatni u ko'rmaydi:

ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyor"; readyAt: string };

function assertNever(value: never): never {
  throw new Error(`Kutilmagan holat: ${JSON.stringify(value)}`);
}

function label(state: OrderState): string {
  switch (state.kind) {
    case "yangi":
      return "Navbatda";
    case "tayyor":
      return `Tayyor: ${state.readyAt}`;
    default:
      return assertNever(state);
  }
}

const fromServer: OrderState = JSON.parse('{"kind":"arxiv"}');
console.log(label({ kind: "yangi" }));
console.log(label(fromServer));

Konsolda:

text
Navbatda
Error: Kutilmagan holat: {"kind":"arxiv"}

tsc — xatosiz: JSON.parse — any. Serverning yangi versiyasi "arxiv" holatini qo'shgan, ilova esa uni bilmaydi. assertNever hech bo'lmasa xatoni aniq qildi. const x: never = state; return x; usulida esa funksiya jim { kind: "arxiv" } obyektini qaytarardi — satr o'rniga obyekt. Ikki yo'l bor: tashqi ma'lumotni kirishda tekshirish (API ma'lumotlarini tiplash) yoki default da xavfsiz standart qiymat qaytarish (vazifalar dagi tartibla kabi — «Vazifalar qadami» da).

6. Ko'p uchraydigan xatolar

6.1 Ajratuvchi maydon literal emas

type A = { kind: string; … } — kind oddiy string bo'lsa, u hech narsani ajratmaydi: if (x.kind === "bekor") toraytirmaydi. Tuzatish: har variantda aniq literal: kind: "bekor".

6.2 Hamma maydonni ixtiyoriy qilish

{ status; cook?; readyAt?; reason? } — imkonsiz holatlar ochiq qoladi. Tuzatish: har holat — alohida variant.

6.3 default da standart qiymat va tekshiruvsiz

default: return "Noma'lum"; — yangi variant qo'shilsa, u jim "Noma'lum" bo'lib chiqadi. Tuzatish: default da never tekshiruvi yoki assertNever.

6.4 Natija turini yozmaslik

Natija turi yozilmasa, TS2366 ham chiqmaydi — funksiya natijasi string | undefined bo'lib qoladi. Tuzatish: natija turini yozing va never tekshiruvini qo'shing.

7. Mashqlar

1-mashq (oson): Variantni tanlang

ts
type Seat =
  | { kind: "stol"; number: number }
  | { kind: "vip"; room: string }
  | { kind: "olib_ketish" };

function seatText(seat: Seat): string {
  switch (seat.kind) {
    case "stol":
      return `Stol ${seat.number}`;
    case "vip":
      return `VIP: ${seat.room}`;
    case "olib_ketish":
      return "Olib ketish";
  }
}
const room: Seat = { kind: "vip", room: "Bahor-1" };
console.log(seatText(room)); // VIP: Bahor-1

Ajratuvchi maydonning nomi: [:kind]. case "stol" ichida seat.room ni o'qisak, xato kodi: TS[:2339]. { kind: "olib_ketish", number: 5 } xatosining kodi: TS.

Yechim
  • Ajratuvchi — kind: hamma variantda bor, har birida boshqa literal.
  • case "stol" ichida seat — { kind: "stol"; number: number }, unda room yo'q: TS2339.
  • olib_ketish variantida number yo'q va obyekt yangi literal — ortiqcha xususiyat: TS2353.

2-mashq (o'rta): To'liqlik tekshiruvini qo'shing

Kodga default va never tekshiruvini qo'shing. Keyin Seat ga { kind: "teras"; table: number } variantini qo'shib, tsc qaysi qatorda va nima deyishini aniqlang. Oxirida terasni ham ko'rib chiqing.

Yechim

teras qo'shilgach, case siz holatda tsc:

text
TS2322: Type '{ kind: "teras"; table: number; }' is not assignable to type 'never'.

Xato — default dagi const unhandled: never = seat; qatorida. Tuzatilgan to'liq variant:

ts
type Seat =
  | { kind: "stol"; number: number }
  | { kind: "vip"; room: string }
  | { kind: "teras"; table: number }
  | { kind: "olib_ketish" };

function seatText(seat: Seat): string {
  switch (seat.kind) {
    case "stol":
      return `Stol ${seat.number}`;
    case "vip":
      return `VIP: ${seat.room}`;
    case "teras":
      return `Teras, stol ${seat.table}`;
    case "olib_ketish":
      return "Olib ketish";
    default: {
      const unhandled: never = seat;
      return unhandled;
    }
  }
}

console.log(seatText({ kind: "teras", table: 3 })); // Teras, stol 3

default endi hech qachon bajarilmaydi — seat u yerda never. Lekin u kelajak uchun "qo'riqchi": keyingi yangi variant ham shu qatorda ushlanadi.

3-mashq (qiyin): Holat mashinasi — to'liq

«Holat mashinasi» bo'limidagi sxema bo'yicha OrderState ni beshta holat bilan yozing (berildi — maydonsiz). Hodisalar ham discriminated union: { type: "boshlash"; cook: string }, { type: "tugatish"; time: string }, { type: "berish" }, { type: "bekor_qilish"; reason: string }. next(state, event) funksiyasi sxemadagi o'tishlarni bajarsin. Ruxsat etilmagan o'tishda — holat o'zgarmasin. state.kind bo'yicha switch to'liq bo'lsin (never bilan).

Yechim
ts
type OrderState =
  | { kind: "yangi" }
  | { kind: "tayyorlanmoqda"; cook: string }
  | { kind: "tayyor"; readyAt: string }
  | { kind: "berildi" }
  | { kind: "bekor"; reason: string };

type OrderEvent =
  | { type: "boshlash"; cook: string }
  | { type: "tugatish"; time: string }
  | { type: "berish" }
  | { type: "bekor_qilish"; reason: string };

function next(state: OrderState, event: OrderEvent): OrderState {
  switch (state.kind) {
    case "yangi":
      if (event.type === "boshlash") {
        return { kind: "tayyorlanmoqda", cook: event.cook };
      }
      if (event.type === "bekor_qilish") {
        return { kind: "bekor", reason: event.reason };
      }
      return state;
    case "tayyorlanmoqda":
      return event.type === "tugatish"
        ? { kind: "tayyor", readyAt: event.time }
        : state;
    case "tayyor":
      return event.type === "berish" ? { kind: "berildi" } : state;
    case "berildi":
    case "bekor":
      return state; // oxirgi holatlar
    default: {
      const unhandled: never = state;
      return unhandled;
    }
  }
}

let order: OrderState = { kind: "yangi" };
order = next(order, { type: "boshlash", cook: "Jasur aka" });
// ruxsat yo'q — holat o'zgarmaydi:
order = next(order, { type: "berish" });
order = next(order, { type: "tugatish", time: "19:40" });
order = next(order, { type: "berish" });
console.log(order); // { kind: 'berildi' }
  • case "berildi": case "bekor": — ikki case bitta blokda (switch dagi "tushib o'tish"): ikkalasi ham oxirgi holat.
  • event.type === "boshlash" dan keyin event.cook ishlatildi — hodisa ham toraydi. Ikki union bir funksiyada, har biri o'z ajratuvchisi bilan.
  • berish hodisasi tayyorlanmoqda holatida e'tiborsiz qoldi: oshxonada hali tayyor bo'lmagan taomni stolga berib bo'lmaydi.

4-mashq: Vazifalar qadami — literal va discriminated union

vazifalar da filtr, saralash, tarmoq holati va xato kodlari hali oddiy string. Ruxsat etilgan qiymatlar faqat izohda: @param {string} filtr "hammasi" | "faol" | "bajarilgan". Bu qadamda ular turlar.ts ga union bo'lib ko'chadi (Union va literal turlar darsining va'dasi), tablar orasidagi xabar — discriminated union bo'ladi, tartibla esa switch va to'liqlik tekshiruviga o'tadi. Ilovada birinchi marta ishlaydigan .ts modul paydo bo'ladi: sinxron.ts.

  • Branch: refactor/union-turlar
  • Commit: refactor: literal va discriminated union, to'liqlik tekshiruvi (never), tanasi: sinxron.ts; Filtr, Saralash, xato kodlari; tartibla switch
  1. turlar.ts ga: Filtr, Saralash, TarmoqHolati, VazifaXatosiKodi, ApiXatosiKodi, SinxronXabar.
  2. royxat.js, saralash.js, vazifa.js, api.js — string o'rniga yangi turlar (JSDoc @param {Filtr} va h.k.).
  3. saralash.js — tartibla if zanjiri o'rniga switch va never bilan.
  4. sinxron.js → sinxron.ts: kelgan xabar unknown, tekshirilgandan keyin SinxronXabar. asosiy.js uni "./sinxron.ts" deb import qiladi.
  5. tsconfig.json — rewriteRelativeImportExtensions; sw.js — VERSIYA = "v5-2"; sw.test.js — .ts manbalarni ham tanisin.
Yechim

turlar.ts ga qo'shilgan turlar:

ts
// Literal turlar union'i: qiymat faqat shulardan biri. Ro'yxatlar
// (FILTRLAR, SARALASHLAR) ham shu turda — biri o'zgarsa, ikkinchisi
// tsc xatosi bilan eslatadi
export type Filtr = "hammasi" | "faol" | "bajarilgan";
export type Saralash = "standart" | "yangi" | "alifbo";
export type TarmoqHolati = "yuklanmoqda" | "tayyor" | "xato" | "oflayn";

/** VazifaXatosi.kod: import/kesh matni nega rad etildi */
export type VazifaXatosiKodi =
  "JSON" | "TUZILMA" | "VERSIYA" | "VAZIFA" | "NOMALUM";

/** ApiXatosi.kod: so'rov nega bajarilmadi */
export type ApiXatosiKodi =
  | "TARMOQ" // internet yo'q, DNS, CORS
  | "VAQT" // 8 s ichida javob kelmadi
  | "BEKOR" // AbortController.abort()
  | "TOPILMADI" // 404
  | "NOTOGRI" // 400, 422
  | "SERVER" // 5xx
  | "HTTP" // boshqa xato status
  | "JAVOB" // tana JSON emas
  | "NOMALUM";

/**
 * Tablar orasidagi xabar (BroadcastChannel). Discriminated union:
 * `tur` maydoni qaysi variant ekanini aytadi. Hozir bitta variant;
 * yangisi qo'shilsa, uni qabul qiluvchi switch'dagi never tekshiruvi
 * "bu turni ishlamading" deb tsc xatosi beradi
 */
export type SinxronXabar = {
  readonly tur: "ozgardi";
  readonly paket: string; // eksportMatni() natijasi
};

Izohdagi ro'yxatlar endi tur: ApiXatosiKodi dagi har qiymat yonida u qachon chiqishi yozilgan. VazifaXatosi va ApiXatosi klasslarida kod maydoni JSDoc bilan shu turni oldi (/** @type {ApiXatosiKodi} */), holatKodi funksiyasi esa @returns {ApiXatosiKodi}. Endi return "TOPILMAD" kabi imlo xatosi tsc dan o'tmaydi.

SinxronXabar — hozircha bitta variantli discriminated union. Nega tur maydoni kerak, agar variant bitta bo'lsa? Ertaga ikkinchi xabar turi qo'shiladi (masalan, "chiqish"). Ajratuvchi maydon oldindan bo'lsa, qabul qiluvchi kod switch (xabar.tur) bilan yoziladi va yangi variant to'liqlik tekshiruvidan o'tadi. Bu switch TypeScript va DOM qadamida asosiy modulida paydo bo'ladi.

tartibla — switch va never. saralash.js hali JavaScript, shuning uchun never JSDoc bilan yoziladi — /** @type {never} */ — const qolmadi: never ning izohdagi shakli:

js
export function tartibla(vazifalar, saralash) {
  switch (saralash) {
    case "standart":
      return vazifalar.toSorted(faollarOldin);
    case "yangi":
      // Bir nechta kalit: birinchisi 0 (teng) bo'lsa, || keyingisiga o'tadi
      return vazifalar.toSorted(
        (a, b) => faollarOldin(a, b) || yangilarOldin(a, b),
      );
    case "alifbo":
      return alifboTartibida(vazifalar);
    default: {
      // To'liqlik tekshiruvi: Saralash'ga yangi qiymat qo'shilib, bu
      // yerda case yozilmasa — saralash never emas, tsc xato beradi.
      // Ish vaqtida esa (JS'dan begona satr kelsa) — "standart"
      /** @type {never} */
      const qolmadi = saralash;
      void qolmadi;
      return vazifalar.toSorted(faollarOldin);
    }
  }
}

void qolmadi; — "o'zgaruvchi ishlatildi" deb belgilash (ESLint "ishlatilmagan o'zgaruvchi" demasin). Blokning oxirgi qatori — «TypeScript nimani ushlamaydi» bo'limidagi ikkinchi yo'l: JavaScript'dan begona satr kelsa (tartibla(v, "boshqa")), ilova yiqilmaydi, "standart" tartibni qo'llaydi. v4.1 ning testi aynan shuni tekshiradi va o'tadi.

Tekshiruv ishlashini sinadik: Saralash ga vaqtincha | "muddat" qo'shildi, case yozilmadi:

text
assets/js/saralash.js(61,13): error TS2322: Type '"muddat"' is not assignable to type 'never'.

Bu darsning «never bilan aniq tekshiruv» bo'limidagi xabar — faqat JavaScript faylda, JSDoc orqali. Qo'shimcha qiymat olib tashlandi, xato 0.

sinxron.ts — birinchi ishlaydigan .ts modul:

ts
// sinxron.ts — bir saytning tablari orasida xabar almashish
import type { SinxronXabar } from "./turlar.ts";

const kanal = new BroadcastChannel("vazifalar");

// Xabar turi: yuboruvchi ham, qabul qiluvchi ham shu nomni ishlatadi.
// const — turi "ozgardi" (literal), string emas
export const OZGARDI = "ozgardi";

export function tarqat(xabar: SinxronXabar): void {
  kanal.postMessage(xabar);
}

// Boshqa tabda ilovaning eski yoki yangi versiyasi ochiq bo'lishi
// mumkin: kelgan ma'lumot — unknown. Bilmagan xabar jim tashlanadi
function xabarmi(malumot: unknown): malumot is SinxronXabar {
  if (
    typeof malumot !== "object" ||
    malumot === null ||
    !("tur" in malumot)
  ) {
    return false;
  }
  return (
    malumot.tur === OZGARDI &&
    "paket" in malumot &&
    typeof malumot.paket === "string"
  );
}

export function tingla(ishlovchi: (xabar: SinxronXabar) => void): void {
  kanal.addEventListener("message", (event) => {
    if (xabarmi(event.data)) {
      ishlovchi(event.data);
    }
  });
}

Bu faylda deyarli hamma o'tgan darslar bor:

  • import type — faqat tur import qilinadi, build'da bu qator o'chadi (verbatimModuleSyntax, type va interface qadami).
  • const OZGARDI = "ozgardi" — literal tur "ozgardi", shuning uchun tarqat({ tur: OZGARDI, … }) SinxronXabar ga mos (Union va literal turlar).
  • event.data — any, uni unknown parametrga beramiz (any va unknown).
  • typeof, === null, "tur" in malumot — ketma-ket toraytirish (Narrowing).
  • ishlovchi: (xabar: SinxronXabar) => void — callback turi (Funksiyalarni tiplash).
  • malumot is SinxronXabar — tur predikati: "true qaytarsam, bu qiymat SinxronXabar". JSDoc'da @returns {qiymat is …} shaklida ko'rgansiz (12/39), .ts dagi to'liq qoidalari — keyingi darsda.

Ilovada kichik xulq farqi bor: buzuq xabar (paket satr emas) endi jim tashlanadi. Oldin u paketniOqi ga yetib, console.warn chiqarardi. Ilovaning o'zi bunday xabar yubormaydi — farq faqat begona yoki eski versiyali tabda seziladi.

JavaScript .ts ni import qiladi. asosiy.js da bitta qator: import { OZGARDI, tarqat, tingla } from "./sinxron.ts";. Brauzer esa .ts ni bilmaydi — dist/ da .js kerak. tsconfig.json ga yangi bayroq:

json
    // import "./sinxron.ts" — fayl nomi aynan; build .js ga almashtiradi
    "rewriteRelativeImportExtensions": true,

Build'dan keyin dist/assets/js/asosiy.js da "./sinxron.js" turibdi — tekshirildi. Testlar (Node) esa manbani o'qiydi, u yerda .ts fayl haqiqatan bor. Shu sabab importlarda fayl nomi aynan yoziladi.

sw.test.js — 2 ta test tahrirlandi. "Har bir modul qobiqda" testi assets/js dagi .js fayllarni sanardi. Endi u .ts manbalarni ham .js nomi bilan qidiradi (sinxron.ts → sinxron.js), turlar.ts va .d.ts esa tashqarida — ular brauzerga yuklanmaydi. "Qobiqdagi fayl diskda bor" testi .js yoki .ts ni qabul qiladi. Sinab ko'rdik: assets/js/x.ts qo'shilsa — AssertionError: assets/js/x.js qobiqda yo'q. v4 testlari ichida tahrirlangani faqat shu ikkitasi.

sw.js da VERSIYA = "v5-2" — build natijasi o'zgardi, kesh yangilanishi kerak.

Tekshiruv: npm test — 137/137, tip, lint, format:check — toza. 159 ta brauzer tekshiruvi dist/ da — 159/159, tablar sinxroni bilan.

Commit va PR:

bash
git switch -c refactor/union-turlar
# turlar.ts, sinxron.ts, saralash.js, JSDoc turlari, tsconfig, sw
npm run build && npm test && npm run tip
npm run lint && npm run format:check
git add .
git commit -m "refactor: literal va discriminated union, to'liqlik tekshiruvi (never)" \
  -m "sinxron.ts; Filtr, Saralash, xato kodlari; tartibla switch"
git push -u origin refactor/union-turlar
gh pr create --fill
gh pr merge --merge

Sarlavha — 70 belgi (≤ 72). Diff: 11 fayl, +144 −52.

8. Real ishda

  • So'rov holati — eng mashhur discriminated union: { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: Error }. React'da (17-qismdan) va TanStack Query kabi kutubxonalarda aynan shu shakl.
  • Redux va reducer'lar — "holat + hodisa → yangi holat" funksiyasi, hodisalar esa { type: "…" } union'i. Bizning next funksiyamiz — reducer'ning o'zi.
  • To'liqlik tekshiruvi — katta kod bazalarida yangi holat qo'shishning xavfsiz yo'li: tsc o'zgartirish kerak bo'lgan hamma joyni ro'yxat qilib beradi.
  • Intervyu: "Discriminated union nima?", "Exhaustiveness checking qanday ishlaydi?", "Nega ixtiyoriy maydonlar o'rniga union?" — o'rta darajadagi TypeScript intervyusining asosiy savollari.

Xulosa

  • Discriminated union — umumiy literal maydonli (kind, type, tur) obyektlar union'i. Har variantda faqat o'ziga tegishli maydonlar — imkonsiz holatlar yozilmaydi.
  • if (x.kind === "…") va switch (x.kind) union'ni aniq variantga toraytiradi.
  • To'liqlik tekshiruvi: default da const unhandled: never = x yoki assertNever(x) — unutilgan variant TS2322/TS2345 beradi.
  • Holat mashinasi: holatlar — discriminated union, o'tishlar — "holat + hodisa → yangi holat" funksiyasi.
  • Tashqi ma'lumot to'liqlik tekshiruvini chetlab o'tadi — kirishda tekshiring yoki default da xavfsiz xulq bering.

Keyingi dars: Custom type guard va assertion funksiyalar — x is T va asserts x is T bilan o'z tekshiruv funksiyalaringizni yozasiz.

Manbalar

  • TypeScript Handbook: "Narrowing" → Discriminated unions, The never type, Exhaustiveness checking — typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
  • TSConfig: rewriteRelativeImportExtensions (TypeScript 5.7) — typescriptlang.org/tsconfig#rewriteRelativeImportExtensions
  • MDN: BroadcastChannel — developer.mozilla.org/docs/Web/API/BroadcastChannel
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
TypeScript discriminated union va to'liqlik tekshiruvi: kind, switch va never — IlmHamroh