IlmHamroh
JavaScript Full-stack/15-qism. TypeScript42/47-dars21 daqiqa
Mundarija (29)

TypeScript'da API ma'lumotlarini tiplash: fetch javobi unknown, tekshiruv va ApiResponse<T>

Qisqacha: fetch javobidagi response.json() TypeScript'da Promise<any> qaytaradi: unga istalgan tur yozsangiz, tsc ishonadi va hech narsa tekshirmaydi. Server shaklni o'zgartirsa, xato ishlash paytida jim chiqadi. To'g'ri yo'l — javobni unknown deb qabul qilish va ish vaqtida tekshirish: tur predikati (value is MenuItem) yoki unknown dan aniq tur qaytaradigan parse… funksiyasi. Generic API klient tekshiruvchini parametr qilib oladi, natijani esa ApiResponse<T> — ok maydonli union — bilan qaytaradi.

Bu darsda

  • Nega const menu: MenuItem[] = await response.json() — tekshiruv emas, va'da ekanini ko'rsatasiz.
  • any ni unknown ga aylantirib, tashqi ma'lumotni majburan tekshirtirasiz.
  • Tur predikati va parse… funksiyasi bilan JSON'ni ish vaqtida tekshirasiz.
  • Tekshiruvchini parametr qilib oladigan generic API klient va ApiResponse<T> yozasiz.
  • vazifalar dagi as ServerRoyxati ni haqiqiy tekshiruvga almashtirasiz.

Oldin bilishingiz kerak: API klient qatlami, any, unknown, never, void va object, Custom type guard va assertion funksiyalar, Discriminated union, TypeScript va DOM.

1. Nega bu kerak?

O'tgan darsda vazifalar ning DOM modullarini tipladik. Bitta joy esa vaqtincha qoldi — server javobi:

ts
const javob = (await api.royxat(sozlama)) as ServerRoyxati;

as — va'da. Bugun shu va'dani tekshiruvga aylantiramiz. Avval muammoni kichik misolda ko'ramiz.

«Bahor» menyusi IlmHamroh'ning mashq API'sidan keladi: https://ilmhamroh.uz/api/mashq/menyu. Javob shunday (API klient qatlami darsidan tanish):

json
[{"id":1,"nom":"Osh","narx":35000,"turkum":"taom"}, …]

Maydon nomlari o'zbekcha — ularni server belgilaydi, biz o'zgartira olmaymiz. Shuning uchun bu darsdagi MenuItem avvalgi darslardagidan (name, price) farq qiladi: tashqi ma'lumotning turi serverning haqiqiy shaklini aynan takrorlashi kerak — nom, narx. Sardor tur yozdi va kunlik hisobni chiqardi. Bir kuni server dasturchisi narxni chiroyli ko'rinsin deb satrga aylantirdi: "35 000". Internetga bog'lanmaslik uchun javobni Response obyekti bilan o'zimiz yasaymiz (Node 24 da ham, brauzerda ham bor):

ts
type MenuItem = { id: number; nom: string; narx: number };

const response = new Response(
  '[{"id":1,"nom":"Osh","narx":"35 000"},' +
  '{"id":3,"nom":"Manti","narx":30000}]',
);
const menu: MenuItem[] = await response.json();
const total = menu.reduce((sum, item) => sum + item.narx, 0);
console.log(total); // 035 00030000

tsc — 0 xato. Natija — "035 00030000": 0 + "35 000" satr yasadi, keyin unga 30000 yopishdi (JavaScript tuzoqlari). Turlar to'g'ri yozilgan, lekin kod noto'g'ri ishladi. Nima bo'ldi?

Hayotiy o'xshatish: «Bahor» ga go'sht yetkazib beruvchi keldi. Qutida "1-nav mol go'shti" deb yozilgan. Tarozida tortmasdan, ko'zdan kechirmasdan omborga qo'ydingiz — chunki yorliqda shunday yozilgan. Tur yozish — yorliqqa ishonish. Tekshiruv — qutini ochib ko'rish.

2. response.json() — any

2.1 any jim tarqaladi

Sabab bitta qatorda. TypeScript'ning lib.dom.d.ts ida (7.0.2):

ts
json(): Promise<any>;

JSON.parse ham any qaytaradi. any — "tekshirma" degani: any ni istalgan turga berish mumkin, uning istalgan xususiyatiga murojaat qilish mumkin. Shuning uchun const menu: MenuItem[] = await response.json() — as siz ham aslida as. Yozilgan tur — tsc ga berilgan va'da, server esa bu va'dadan bexabar.

TypeScript tarmoqdan nima kelishini bilishi mumkin emas. Server boshqa kompyuterda, boshqa dasturchi yozgan, ertaga o'zgarishi mumkin. Nega TypeScript darsidagi qoida: turlar ishlash paytida yo'q. Tashqi ma'lumot — dasturning "chegarasi": u yerda faqat kod bilan tekshiriladi.

Bu darsda quradigan yo'l:

flowchart TB
  A["Server JSON"] --> B["response.json()<br/>any"]
  B --> C["data: unknown"]
  C --> D["parseMenu(data)<br/>tekshiruv"]
  D -->|to'g'ri| E["MenuItem[]"]
  D -->|buzuq| F["TypeError<br/>sababi bilan"]

Sxemaga qarang: any faqat bitta qatorda yashaydi va darhol unknown ga aylanadi. Keyingi kod faqat tekshiruvdan o'tgan MenuItem[] ni ko'radi.

2.2 unknown — "avval tekshir"

Birinchi qadam — any ni darhol unknown ga aylantirish:

ts
const response = new Response('[{"id":1,"nom":"Osh","narx":35000}]');
const data: unknown = await response.json();
console.log(data.length);
const first = data[0];

Kompilyator:

text
TS18046: 'data' is of type 'unknown'.
TS18046: 'data' is of type 'unknown'.

Tarjimasi: "data ning turi unknown". Bu tur — any ning xavfsiz egizi. Unga hamma narsani berish mumkin, lekin undan hech narsani olib bo'lmaydi: avval toraytirish kerak. tsc endi bizni tekshiruv yozishga majbur qiladi. vazifalar ning api.ts ida javobniOqi aynan shunday: Utility types qadamidan beri u Promise<unknown> qaytaradi.

Tekshirib ko'ring: const menu = (await response.json()) as MenuItem[]; va const menu: MenuItem[] = await response.json(); — farqi bormi?

Javob

Amalda yo'q: ikkalasi ham hech narsani tekshirmaydi. Ikkinchisida as ko'rinmaydi, chunki json() — any, any esa istalgan turga jim beriladi. Birinchisi hech bo'lmasa ochiq yozilgan — kod sharhida ko'zga tashlanadi. Ikkalasining o'rniga — unknown va tekshiruv.

3. Ish vaqtida tekshirish

3.1 Tur predikati

Custom type guard darsidagi value is T — tashqi ma'lumot uchun eng tabiiy vosita:

ts
type MenuItem = { id: number; nom: string; narx: number };

function isMenuItem(value: unknown): value is MenuItem {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "number" &&
    "nom" in value &&
    typeof value.nom === "string" &&
    "narx" in value &&
    typeof value.narx === "number"
  );
}

console.log(isMenuItem({ id: 1, nom: "Osh", narx: 35000 })); // true
const broken = { id: 1, nom: "Osh", narx: "35 000" };
console.log(isMenuItem(broken)); // false

Har "id" in value dan keyin tsc value ni toraytiradi: "id xususiyati bor obyekt". Keyin typeof value.id === "number" uni yanada aniqlaydi. Natijada return dagi butun ifoda true bo'lsa — qiymat haqiqatan MenuItem shaklida.

Diqqat: Tur predikati ham va'da. Funksiya ichida narx ni tekshirishni unutsangiz, tsc buni sezmaydi — value is MenuItem deb yozdingiz, u ishonadi. Shuning uchun predikatlarni test bilan tekshiring: to'g'ri obyekt va har maydoni buzilgan obyekt.

3.2 parse… funksiyasi — tekshir yoki xato tashla

Predikat true/false beradi. Ko'pincha esa bizga sabab kerak: qaysi maydon buzuq? Buning uchun unknown oladigan va aniq tur qaytaradigan (yoki xato tashlaydigan) funksiya qulay:

ts
type MenuItem = { id: number; nom: string; narx: number };

function parseMenu(data: unknown): MenuItem[] {
  if (!Array.isArray(data)) {
    throw new TypeError("Menyu massiv emas");
  }
  return data.map((item: unknown, i) => {
    if (typeof item !== "object" || item === null) {
      throw new TypeError(`${i + 1}-taom obyekt emas`);
    }
    const { id, nom, narx } = item as Record<string, unknown>;
    if (typeof id !== "number" || typeof nom !== "string") {
      throw new TypeError(`${i + 1}-taom: id yoki nom buzuq`);
    }
    if (typeof narx !== "number" || narx < 0) {
      throw new TypeError(`${i + 1}-taom: narx son emas`);
    }
    return { id, nom, narx };
  });
}

const raw: unknown = JSON.parse(
  '[{"id":1,"nom":"Osh","narx":"35 000"}]',
);
try {
  parseMenu(raw);
} catch (error) {
  console.log((error as Error).message); // 1-taom: narx son emas
}

Uch narsaga e'tibor bering:

  • Array.isArray(data) dan keyin data — any[]. Biz item: unknown deb yozib, any ni darhol to'xtatdik — aks holda item.narx tekshiruvsiz o'tib ketardi.
  • as Record<string, unknown> — as bor, lekin xavfsiz: oldingi qatorda obyekt ekani tekshirildi, va unknown qiymatlar baribir tekshiriladi.
  • Qaytgan obyekt yangi: { id, nom, narx }. Serverdan kelgan ortiqcha maydonlar (turkum, yoki kimdir qo'shgan __proto__) ichkariga o'tmaydi — prototype pollution himoyasi. vazifalar ning vazifaniOl funksiyasi ham "oq ro'yxat" — faqat kerakli maydonlarni oladi.

Endi «Nega bu kerak?» dagi xato 035 00030000 emas, aniq xabar: 1-taom: narx son emas. Xato tarmoq chegarasida, sababi bilan chiqdi — chuqur hisob-kitobning ichida emas.

Tekshirib ko'ring: catch (error) ichida (error as Error).message yozdik. Bu xavfsizmi?

Javob

Bu misolda — ha, chunki try ichida faqat parseMenu bor va u faqat TypeError tashlaydi. Umumiy holda catch ga istalgan narsa kelishi mumkin (throw "satr" ham), shuning uchun error instanceof Error bilan tekshirish to'g'riroq. catch dagi unknown ni to'g'ri tiplash — Async kod va xatolarni tiplash darsida.

3.3 Chegaralar ro'yxati

fetch — yagona "tashqi eshik" emas. TypeScript ko'rmaydigan, shuning uchun ish vaqtida tekshiriladigan hamma joylar:

Manba Turi vazifalar da tekshiruv
response.json(), JSON.parse any serverRoyxatiniTekshir, paketniOqi
localStorage.getItem string | null saqlash.ts (keyin paketniOqi)
URLSearchParams.get string | null marshrut.ts — oq ro'yxat
BroadcastChannel xabari any sinxron.ts — xabarmi predikati
fayl matni, FormData string, File fayl.ts, import

Qoida oddiy: ma'lumot sizning kodingizdan tashqarida yaratilgan bo'lsa, u tekshirilmagan. Tashqari — bu foydalanuvchi, server, boshqa tab yoki diskdagi eski versiya. vazifalar da bu tekshiruvlarning hammasi bir necha modulga yig'ilgan, qolgan kod esa tekshirilgan turlar bilan xotirjam ishlaydi. Boshqa tab — ilovaning eski versiyasi bo'lishi ham mumkin: uning xabari yangi kod kutgan shaklda bo'lmasligi tabiiy.

Kesh ham tashqi manba. Ilova yangilangach, localStorage dagi o'tgan haftaning ma'lumoti yangi turga mos kelmasligi mumkin. Shuning uchun vazifalar keshni ham import qoidalari bilan o'qiydi va buzuq bo'lsa — e'tiborsiz qoldiradi.

4. Generic API klient

4.1 Yolg'onchi generic

Har fetch atrofida bir xil kod takrorlanmasin deb, ko'p loyihada shunday yordamchi uchraydi:

ts
async function getJson<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json(); // any → T, tekshiruvsiz
}

const menu = await getJson<MenuItem[]>("/api/mashq/menyu");

Chiroyli ko'rinadi: chaqiruvchi turni beradi, natija aniq turli. Lekin bu — as ning generic ko'rinishi. T — faqat chaqiruvchining so'zi, getJson uni hech qayerda tekshirmaydi. typescript-eslint'ning no-unsafe-return qoidasi aynan shu qatorni ushlaydi ("any qiymat qaytarildi").

4.2 Tekshiruvchi — parametr

To'g'ri yo'l: turni chaqiruvchining so'zidan emas, tekshiruvchidan chiqarish. Klient parse funksiyasini oladi, T esa shu funksiyaning natija turidan chiqariladi:

ts
type MenuItem = { id: number; nom: string; narx: number };

async function getJson<T>(
  response: Response,
  parse: (data: unknown) => T,
): Promise<T> {
  const data: unknown = await response.json();
  return parse(data);
}

function parseMenuItem(data: unknown): MenuItem {
  if (typeof data !== "object" || data === null) {
    throw new TypeError("Taom obyekt emas");
  }
  const { id, nom, narx } = data as Record<string, unknown>;
  if (typeof id !== "number" || typeof nom !== "string" ||
      typeof narx !== "number") {
    throw new TypeError("Taom maydonlari buzuq");
  }
  return { id, nom, narx };
}

const osh = await getJson(
  new Response('{"id":1,"nom":"Osh","narx":35000,"turkum":"taom"}'),
  parseMenuItem,
);
console.log(osh); // { id: 1, nom: 'Osh', narx: 35000 }

getJson ga <MenuItem> yozmadik — T parseMenuItem ning natija turidan chiqdi. Endi T ni "yolg'on" qilishning yagona yo'li — parse funksiyasini noto'g'ri yozish, va u alohida test qilinadi. (Darsda tarmoqsiz ishlash uchun getJson Response oladi. Haqiqiy klientda u url ni oladi va ichida fetch qiladi — 3-mashqda.)

Keyingi darsda ko'rasiz: Zod kutubxonasining sxemasi aynan shunday parse(data: unknown): T funksiyasini beradi — va T ni sxemaning o'zidan chiqaradi.

5. ApiResponse<T> — xato ham natija

5.1 Server xatosi ham shakl

Mashq API'da yo'q taomni so'rasak (/menyu/99), server 404 holati va shunday tana qaytaradi:

json
{"xato":"Taom topilmadi","id":99}

Bu ham tashqi ma'lumot — u ham tekshiriladi. Chaqiruvchiga natijani ikki holatli discriminated union bilan qaytaramiz:

ts
type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; status: number; message: string };

type MenuItem = { id: number; nom: string; narx: number };

function errorMessage(data: unknown): string {
  if (typeof data === "object" && data !== null && "xato" in data &&
      typeof data.xato === "string") {
    return data.xato;
  }
  return "Noma'lum server xatosi";
}

async function readResponse<T>(
  response: Response,
  parse: (data: unknown) => T,
): Promise<ApiResponse<T>> {
  const data: unknown = await response.json();
  if (!response.ok) {
    const message = errorMessage(data);
    return { ok: false, status: response.status, message };
  }
  return { ok: true, data: parse(data) };
}

const result = await readResponse(
  new Response('{"xato":"Taom topilmadi","id":99}', { status: 404 }),
  (data): MenuItem => data as MenuItem, // faqat misol uchun
);
if (result.ok) {
  console.log(result.data.nom);
} else {
  console.log(result.status, result.message); // 404 Taom topilmadi
}
  • ok — ajratuvchi maydon (discriminant). if (result.ok) ichida tsc data borligini, else da status va message borligini biladi.
  • ApiResponse<T> — generic tur: ApiResponse<MenuItem>, ApiResponse<MenuItem[]> — bitta shakl, turli ma'lumot.
  • Oxiridagi data as MenuItem — faqat bu misolni qisqa qilish uchun; haqiqiy kodda parseMenuItem.

5.2 Throw yoki ApiResponse?

API klient qatlami darsida xatolar ApiXatosi bilan tashlanardi. ApiResponse<T> esa xatoni qaytaradi. Ikkala uslub ham real loyihalarda uchraydi:

throw ApiXatosi ApiResponse<T>
Unutish try siz — ushlanmagan xato result.data — tsc to'xtatadi
Kod qisqaroq har chaqiruvda if (result.ok)
Qachon kutilmagan xato kutilgan holat (404, 422)

ok ni tekshirmasdan data ga murojaat qilsangiz:

ts
type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; status: number; message: string };

declare const result: ApiResponse<string[]>;
console.log(result.data.length);

Kompilyator:

text
TS2339: Property 'data' does not exist on type 'ApiResponse<string[]>'.
  Property 'data' does not exist on type '{ ok: false; status: number; message: string; }'.

TypeScript aytyapti: "ApiResponse<string[]> turida data yo'q" — pastki qator aniqlashtiradi: "ok: false holatida data yo'q".

Bu ApiResponse ning asosiy foydasi: xato holatini "unutib" bo'lmaydi. (declare const — "shunday o'zgaruvchi bor deb hisobla" degan e'lon, faqat tur tekshiruvi uchun, Declaration fayllari darsidan.) Bu g'oyani umumiy ko'rinishga — Result<T, E> ga — Async kod va xatolarni tiplash darsida keltiramiz.

6. Tuzoqlar: JSON nimani yo'qotadi

Tekshiruvni yozayotganda JSON'ning o'z cheklovlarini unutmang:

  • Sana — satr. {"sana":"2026-10-06T10:00:00.000Z"} — Date emas. type Order = { sana: Date } deb yozib, order.sana.getTime() chaqirsangiz — TypeError. Tekshiruvda new Date(…) ga aylantiring va Number.isNaN(d.getTime()) ni tekshiring.
  • Katta son — JSON'da id: 9007199254740993 aniq saqlanmaydi (Number turi). Bunday id'larni server ko'pincha satr qilib yuboradi.
  • null va yo'q maydon — farqli: {"izoh":null} va {}. Turingiz izoh?: string bo'lsa, null unga mos emas — tekshiruv ikkalasini ham hal qilsin.
  • Ortiqcha maydonlar — tur ularni ko'rmaydi, lekin obyektda turadi. { ...data } bilan nusxalasangiz, ular ham ko'chadi. Oq ro'yxat bilan yangi obyekt yasang.

7. Ko'p uchraydigan xatolar

7.1 const x: T = await response.json()

Tekshiruv yo'q, faqat va'da. Tuzatish: const data: unknown = await response.json() va parse….

7.2 TS18046: '…' is of type 'unknown'.

Bu xato emas — yaxshi belgi: tsc tekshiruv so'rayapti. Tuzatish: typeof, Array.isArray, in, instanceof bilan toraytiring yoki parse… ga bering. as bilan "tinchlantirmang".

7.3 Generic getJson<T> tekshiruvsiz

Tuzatish: klient parse: (data: unknown) => T ni olsin.

7.4 Predikat yarim tekshiradi

value is MenuItem ichida bitta maydon unutilgan. Tuzatish: har maydon buzilgan holat uchun test.

8. Mashqlar

1-mashq (oson): Qaysi biri tekshiradi?

Quyidagilardan qaysi biri ishlash paytida ma'lumotni tekshiradi? (a) await response.json() as MenuItem[]; (b) const menu: MenuItem[] = await response.json(); (c) parseMenu(await response.json()); (d) getJson<MenuItem[]>(url) (tekshiruvchisiz). Javob (bitta harf): .

Yechim

Faqat (c). (a) va (b) — any ni turga berish, tsc ga va'da. (d) — o'sha va'da, generic ichida yashiringan. parseMenu esa har maydonni typeof bilan tekshiradi va buzuq ma'lumotda xato tashlaydi.

2-mashq (o'rta): isOrder

«Bahor» buyurtma API'si shunday obyekt qaytaradi: { "id": 15, "taomlar": ["Osh", "Manti"], "holat": "tayyor" }. holat — faqat "yangi", "tayyorlanmoqda", "tayyor", "berildi" (Union va literal turlar darsidagi OrderStatus holatlari). Tur va tur predikatini yozing. Ishora: massiv elementlarini every bilan tekshiring; holatlar ro'yxatini as const massiv qiling (Massiv va tuple) va qiymatni undagi har holat bilan some orqali solishtiring (nega includes emas — yechimda).

Yechim
ts
const STATUSES = [
  "yangi",
  "tayyorlanmoqda",
  "tayyor",
  "berildi",
] as const;
type OrderStatus = (typeof STATUSES)[number];
type Order = {
  id: number;
  taomlar: string[];
  holat: OrderStatus;
};

function isStatus(value: unknown): value is OrderStatus {
  return STATUSES.some((status) => status === value);
}

function isOrder(value: unknown): value is Order {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "number" &&
    "taomlar" in value &&
    Array.isArray(value.taomlar) &&
    value.taomlar.every((t: unknown) => typeof t === "string") &&
    "holat" in value &&
    isStatus(value.holat)
  );
}

const raw: unknown = JSON.parse(
  '{"id":15,"taomlar":["Osh","Manti"],"holat":"tayyor"}',
);
console.log(isOrder(raw)); // true
const wrong = { id: 15, taomlar: ["Osh"], holat: "pishdi" };
console.log(isOrder(wrong)); // false
  • (typeof STATUSES)[number] — massivdagi qiymatlardan union: "yangi" | "tayyorlanmoqda" | "tayyor" | "berildi" (indexed access). Ro'yxat va tur — bitta joyda.
  • STATUSES.includes(value) yozib bo'lmaydi: includes faqat OrderStatus qabul qiladi, value esa unknown (tsc — TS2345). some bilan === solishtirish — har qanday turni oladi.
  • every((t: unknown) => …) — value.taomlar any[], elementga unknown deb tur berib, any ni to'xtatdik.

3-mashq (qiyin): Amaliy tajriba — menyu klienti

kurs/mashqlar/15/42-api/menu-api.ts:

  • parseMenu(data: unknown): MenuItem[] — «parse… funksiyasi» bo'limidagi kabi; MenuItem ga turkum: "taom" | "ichimlik" ni ham qo'shing.
  • createMenuApi(fetchFn: typeof fetch, baseUrl: string) — { list(): Promise<ApiResponse<MenuItem[]>> } qaytaradi. fetchFn — tashqaridan beriladi (tikuv — vazifalar dagi apiKlient({ fetch }) kabi), testda soxta fetch qo'yamiz.
  • menu-api.test.ts — soxta fetch bilan uch test: to'g'ri javob; narx satr bo'lgan javob — TypeError; 500 holat — { ok: false, status: 500 }.

Ixtiyoriy: haqiqiy API bilan sinab ko'ring — createMenuApi(fetch, "https://ilmhamroh.uz/api/mashq").

Yechim

42-api/menu-api.ts:

ts
// 42-api/menu-api.ts — «Bahor» menyu klienti: tarmoq + tekshiruv
export type MenuItem = {
  readonly id: number;
  readonly nom: string;
  readonly narx: number;
  readonly turkum: "taom" | "ichimlik";
};

export type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; status: number; message: string };

function parseItem(item: unknown, i: number): MenuItem {
  if (typeof item !== "object" || item === null) {
    throw new TypeError(`${i + 1}-taom obyekt emas`);
  }
  const { id, nom, narx, turkum } = item as Record<string, unknown>;
  if (typeof id !== "number" || typeof nom !== "string") {
    throw new TypeError(`${i + 1}-taom: id yoki nom buzuq`);
  }
  if (typeof narx !== "number" || narx < 0) {
    throw new TypeError(`${i + 1}-taom: narx son emas`);
  }
  if (turkum !== "taom" && turkum !== "ichimlik") {
    throw new TypeError(`${i + 1}-taom: turkum noma'lum`);
  }
  return { id, nom, narx, turkum };
}

export function parseMenu(data: unknown): MenuItem[] {
  if (!Array.isArray(data)) {
    throw new TypeError("Menyu massiv emas");
  }
  return data.map((item: unknown, i) => parseItem(item, i));
}

export function createMenuApi(fetchFn: typeof fetch, base: string) {
  return {
    async list(): Promise<ApiResponse<MenuItem[]>> {
      const response = await fetchFn(`${base}/menyu`);
      if (!response.ok) {
        const message = `HTTP ${response.status}`;
        return { ok: false, status: response.status, message };
      }
      const data: unknown = await response.json();
      return { ok: true, data: parseMenu(data) };
    },
  };
}

const api = createMenuApi(
  async () => new Response('[{"id":4,"nom":"Ko\'k choy",' +
    '"narx":5000,"turkum":"ichimlik"}]'),
  "https://example.com",
);
const result = await api.list();
console.log(result.ok && result.data[0]?.nom); // Ko'k choy

turkum !== "taom" && turkum !== "ichimlik" dan keyin turkum — "taom" | "ichimlik" (unknown dan literal union'ga toraytirildi). createMenuApi ning natija turi yozilmadi — list imzosidan chiqariladi. Oxirgi qatorlar — darsda sinash uchun, mashq faylida ular yo'q.

42-api/menu-api.test.ts:

ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createMenuApi } from "./menu-api.ts";

// Soxta fetch: har chaqiruvda tayyor javob qaytaradi
function fakeFetch(body: string, status = 200): typeof fetch {
  return async () => new Response(body, { status });
}

const OSH = '{"id":1,"nom":"Osh","narx":35000,"turkum":"taom"}';
const BASE = "https://example.com";

test("to'g'ri javob — tekshirilgan menyu", async () => {
  const api = createMenuApi(fakeFetch(`[${OSH}]`), BASE);
  const result = await api.list();
  assert.ok(result.ok);
  assert.equal(result.data[0]?.narx, 35000);
});

test("narx satr — TypeError", async () => {
  const body = OSH.replace("35000", '"35 000"');
  const api = createMenuApi(fakeFetch(`[${body}]`), BASE);
  await assert.rejects(api.list(), /narx son emas/);
});

test("500 — ok: false", async () => {
  const api = createMenuApi(fakeFetch("{}", 500), BASE);
  assert.deepEqual(await api.list(), {
    ok: false,
    status: 500,
    message: "HTTP 500",
  });
});

assert.ok(result.ok) — Node'ning assert.ok i TypeScript uchun assertion funksiya (asserts value): keyingi qatorda result.data ga murojaat qilish mumkin. npx tsc --pretty false — toza, node --test 42-api/menu-api.test.ts — bizda (Node 24.21.0):

text
✔ to'g'ri javob — tekshirilgan menyu (30.1498ms)
✔ narx satr — TypeError (0.8822ms)
✔ 500 — ok: false (1.1258ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 182.9084

Haqiqiy API bilan (createMenuApi(fetch, "https://ilmhamroh.uz/api/mashq")) — ok: true va 4 taom: Osh, Lag'mon, Manti, Ko'k choy. XATOLAR.md ga TS18046 (tashqi ma'lumot) va TS2339 (ok tekshirilmagan ApiResponse) ni qo'shing:

bash
git add 15/42-api 15/XATOLAR.md
git commit -m "15/42: menyu klienti — parseMenu va ApiResponse"

4-mashq: Vazifalar qadami — server javobini tekshirish

O'tgan qadamda serverRoyxatiniOl server javobini as ServerRoyxati bilan "qabul qildi". Endi javob unknown → ish vaqtida tekshiriladi. Bitta qadam ikki darsni qamraydi: bu dars (tekshiruv) va keyingisi (Zod o'lchovi).

  • Branch: refactor/api-tekshiruv
  • Commit: refactor: server javobi unknown — serverRoyxatiniTekshir bilan, tanasi: paket.ts; as olib tashlandi; Zod o'lchandi (sinov/15/olchov/zod)
  1. paket.js → paket.ts (git mv), npm run tip — xatolarni sanang va tuzating.
  2. paket.ts ga serverRoyxatiniTekshir(malumot: unknown): ServerRoyxati — obyekt, vazifalar massiv, versiya ≥ 0 butun son, har vazifa — import qoidalari bilan.
  3. asosiy.ts dagi ikkala as ServerRoyxati ni shu funksiyaga almashtiring.
  4. tekshiruv/paket.test.js ga 4 test.
Yechim

1-qadam. paket.ts nomi o'zgartirilganda — 10 xato: TS7006 (parametr turi yo'q) 9 ta va bitta TS7053 (Record ga satr bilan murojaat). JSDoc'dagi @param turlari parametrlarning yoniga ko'chdi — xato 0.

2-qadam: serverRoyxatiniTekshir. paket.ts ichida import uchun allaqachon tekshiruvchi bor edi (vazifalarniTekshir, Custom type guard darsidagi oddiyObyektmi va malumotlarniTekshir). Server javobi ham xuddi shu qoidalardan o'tadi:

ts
/** Server ro'yxati (GET va PUT javobi): versiya — server hisobi */
export type ServerRoyxati = {
  readonly vazifalar: VazifaMalumoti[];
  readonly versiya: number;
};

/**
 * Server javobi — unknown: TS uni ko'rmaydi, tur yozish uni
 * tekshirmaydi. Shakl shu yerda, ish vaqtida tekshiriladi; vazifalar —
 * import bilan bir xil qoidalar
 * @throws {VazifaXatosi} kod: TUZILMA | VAZIFA
 */
export function serverRoyxatiniTekshir(
  malumot: unknown,
): ServerRoyxati {
  if (!oddiyObyektmi(malumot) || !Array.isArray(malumot.vazifalar)) {
    throw new VazifaXatosi("Server javobida vazifalar ro'yxati yo'q.", {
      kod: "TUZILMA",
    });
  }
  const { versiya } = malumot;
  if (
    typeof versiya !== "number" ||
    !Number.isInteger(versiya) ||
    versiya < 0
  ) {
    throw new VazifaXatosi("Server javobida versiya yo'q.", {
      kod: "TUZILMA",
    });
  }
  return { vazifalar: vazifalarniTekshir(malumot.vazifalar), versiya };
}

oddiyObyektmi — tur predikati (qiymat is Record<string, unknown>), shuning uchun keyingi qatorda malumot.vazifalar ga murojaat mumkin. vazifalarniTekshir — har vazifadan faqat id, matn, bajarildi ni oladi (oq ro'yxat) va assertion funksiya bilan tekshiradi. Natija — yangi obyekt.

3-qadam: asosiy.ts. Oldin va keyin:

ts
// Oldin (#41)
const javob = (await api.royxat(sozlama)) as ServerRoyxati;
let { vazifalar } = javob;
if (javob.versiya === 0) { /* … as ServerRoyxati … */ }
return paketniOqi(eksportMatni(vazifalar)); // JSON aylanma

// Keyin
const javob = serverRoyxatiniTekshir(await api.royxat(sozlama));
if (javob.versiya !== 0) {
  return javob.vazifalar;
}
const joriy = holat.vazifalar.toJSON();
return serverRoyxatiniTekshir(await api.almashtir(joriy, sozlama))
  .vazifalar;

Oldingi versiyada bir hiyla bor edi: vazifalarni tekshirish uchun ular JSON'ga aylantirilib, paketniOqi dan qayta o'tkazilardi ("JSON aylanma"). Endi bevosita tekshiriladi. apiKlient o'zi transport bo'lib qoladi — unknown qaytaradi, tekshiruv paket.ts da. Shuning uchun api.test.js o'zgarmadi.

4-qadam: testlar (tekshiruv/paket.test.js, JavaScript qoladi):

js
// 15/#42: server javobi (unknown) — ish vaqtida tekshiriladi
describe("serverRoyxatiniTekshir", () => {
  test("to'g'ri javob — faqat uch maydon qoladi", () => {
    const javob = serverRoyxatiniTekshir({
      vazifalar: [{ id: 2, matn: "Non", bajarildi: true, egasi: "x" }],
      versiya: 5,
    });
    assert.deepEqual(javob, {
      vazifalar: [{ id: 2, matn: "Non", bajarildi: true }],
      versiya: 5,
    });
  });
  // + "TUZILMA" (obyekt emas, versiya buzuq) va "VAZIFA" testlari
});

Birinchi testga qarang: serverdan kelgan begona egasi maydoni natijaga o'tmadi.

Xulqdagi farq (faqat buzuq javobda): versiya siz server javobi endi rad etiladi — ilova "xato" holatiga o'tadi. Oldin u jim o'tib ketardi.

Natija: npm test — 141/141 (+4). tip, lint, format:check — toza. Brauzer tekshiruvi (dist/, haqiqiy API) — 159/159. Diff: 6 fayl, +140 −67.

bash
git switch -c refactor/api-tekshiruv
git mv assets/js/paket.js assets/js/paket.ts
npm run build && npm test && npm run tip
npm run lint && npm run format:check
git add .
git commit \
  -m "refactor: server javobi unknown — serverRoyxatiniTekshir bilan" \
  -m "paket.ts; as olib tashlandi; Zod o'lchandi (sinov/15/olchov/zod)"
git push -u origin refactor/api-tekshiruv
gh pr create --fill
gh pr merge --merge

Commit tanasidagi "Zod o'lchandi" — keyingi dars mavzusi: nega qo'lda yozilgan tekshiruv qoldi va kutubxona nima berardi.

9. Real ishda

  • Har frontend serverdan ma'lumot oladi va har birida "chegara" bor. Yaxshi kod bazasida u bitta qatlamda — API klientda: unknown kiradi, tekshirilgan tur chiqadi.
  • Kod generatsiyasi. Katta jamoalar turlarni qo'lda yozmaydi: server OpenAPI (Swagger) tavsifidan openapi-typescript kabi vositalar turlarni yasaydi. Lekin bu ham tur — server boshqacha javob bersa, tekshiruv baribir kerak.
  • Sxema kutubxonalari — Zod, Valibot: tekshiruvchi va turni bitta joyda yozish (keyingi dars).
  • Intervyu: "response.json() qanday tur qaytaradi?", "any va unknown farqi?", "API javobini TypeScript'da qanday validatsiya qilasiz?".

Xulosa

  • response.json() va JSON.parse — any: yozilgan tur tekshiruv emas, va'da. Server o'zgarsa — jim xato.
  • Tashqi ma'lumot — unknown: tsc tekshiruv yozishga majbur qiladi (TS18046).
  • Tekshiruv: tur predikati (value is T) yoki parse(data: unknown): T — xato bo'lsa sababi bilan tashlaydi va faqat kerakli maydonlarni oladi.
  • Generic klient turni chaqiruvchidan emas, parse funksiyasidan olsin. ApiResponse<T> — ok maydonli union: xato holatini unutib bo'lmaydi.
  • JSON tuzoqlari: sana — satr, katta son, null va yo'q maydon, ortiqcha maydonlar.

Keyingi dars: Zod va turni sxemadan chiqarish — tekshiruv va turni bitta sxemada yozamiz: z.object, z.infer, safeParse, va kutubxonaning brauzerdagi narxini o'lchaymiz.

Manbalar

  • TypeScript Handbook: "Narrowing" (in, type predicates) — typescriptlang.org/docs/handbook/2/narrowing.html
  • TypeScript 4.9 Release Notes: "Unlisted Property Narrowing with the in Operator"
  • MDN: "Response.json()", "Response()" — developer.mozilla.org
  • typescript-eslint: no-unsafe-return, no-unsafe-assignment — typescript-eslint.io/rules
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
TypeScript'da API ma'lumotlarini tiplash: fetch javobi unknown, tekshiruv va ApiResponse<T> — IlmHamroh