Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. response.json() — any
- 2.1 any jim tarqaladi
- 2.2 unknown — "avval tekshir"
- 3. Ish vaqtida tekshirish
- 3.1 Tur predikati
- 3.2 parse… funksiyasi — tekshir yoki xato tashla
- 3.3 Chegaralar ro'yxati
- 4. Generic API klient
- 4.1 Yolg'onchi generic
- 4.2 Tekshiruvchi — parametr
- 5. ApiResponse<T> — xato ham natija
- 5.1 Server xatosi ham shakl
- 5.2 Throw yoki ApiResponse?
- 6. Tuzoqlar: JSON nimani yo'qotadi
- 7. Ko'p uchraydigan xatolar
- 7.1 const x: T = await response.json()
- 7.2 TS18046: '…' is of type 'unknown'.
- 7.3 Generic getJson<T> tekshiruvsiz
- 7.4 Predikat yarim tekshiradi
- 8. Mashqlar
- 1-mashq (oson): Qaysi biri tekshiradi?
- 2-mashq (o'rta): isOrder
- 3-mashq (qiyin): Amaliy tajriba — menyu klienti
- 4-mashq: Vazifalar qadami — server javobini tekshirish
- 9. Real ishda
- Xulosa
- Manbalar
TypeScript'da API ma'lumotlarini tiplash: fetch javobi unknown, tekshiruv va ApiResponse<T>
Qisqacha:
fetchjavobidagiresponse.json()TypeScript'daPromise<any>qaytaradi: unga istalgan tur yozsangiz,tscishonadi va hech narsa tekshirmaydi. Server shaklni o'zgartirsa, xato ishlash paytida jim chiqadi. To'g'ri yo'l — javobniunknowndeb qabul qilish va ish vaqtida tekshirish: tur predikati (value is MenuItem) yokiunknowndan aniq tur qaytaradiganparse…funksiyasi. Generic API klient tekshiruvchini parametr qilib oladi, natijani esaApiResponse<T>—okmaydonli union — bilan qaytaradi.
Bu darsda
- Nega
const menu: MenuItem[] = await response.json()— tekshiruv emas, va'da ekanini ko'rsatasiz. anyniunknownga 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. vazifalardagias ServerRoyxatini 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:
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):
[{"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):
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 00030000tsc — 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):
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:
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:
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[];vaconst 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:
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)); // falseHar "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
narxni tekshirishni unutsangiz,tscbuni sezmaydi —value is MenuItemdeb 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:
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 keyindata—any[]. Bizitem: unknowndeb yozib,anyni darhol to'xtatdik — aks holdaitem.narxtekshiruvsiz o'tib ketardi.as Record<string, unknown>—asbor, lekin xavfsiz: oldingi qatorda obyekt ekani tekshirildi, vaunknownqiymatlar baribir tekshiriladi.- Qaytgan obyekt yangi:
{ id, nom, narx }. Serverdan kelgan ortiqcha maydonlar (turkum, yoki kimdir qo'shgan__proto__) ichkariga o'tmaydi — prototype pollution himoyasi.vazifalarningvazifaniOlfunksiyasi 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).messageyozdik. 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:
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:
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:
{"xato":"Taom topilmadi","id":99}Bu ham tashqi ma'lumot — u ham tekshiriladi. Chaqiruvchiga natijani ikki holatli discriminated union bilan qaytaramiz:
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)ichidatscdataborligini,elsedastatusvamessageborligini 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 koddaparseMenuItem.
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:
type ApiResponse<T> =
| { ok: true; data: T }
| { ok: false; status: number; message: string };
declare const result: ApiResponse<string[]>;
console.log(result.data.length);Kompilyator:
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"}—Dateemas.type Order = { sana: Date }deb yozib,order.sana.getTime()chaqirsangiz —TypeError. Tekshiruvdanew Date(…)ga aylantiring vaNumber.isNaN(d.getTime())ni tekshiring. - Katta son — JSON'da
id: 9007199254740993aniq saqlanmaydi (Number turi). Bunday id'larni server ko'pincha satr qilib yuboradi. nullva yo'q maydon — farqli:{"izoh":null}va{}. Turingizizoh?: stringbo'lsa,nullunga 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
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:includesfaqatOrderStatusqabul qiladi,valueesaunknown(tsc—TS2345).somebilan===solishtirish — har qanday turni oladi.every((t: unknown) => …)—value.taomlarany[], elementgaunknowndeb tur berib,anyni 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;MenuItemgaturkum: "taom" | "ichimlik"ni ham qo'shing.createMenuApi(fetchFn: typeof fetch, baseUrl: string)—{ list(): Promise<ApiResponse<MenuItem[]>> }qaytaradi.fetchFn— tashqaridan beriladi (tikuv —vazifalardagiapiKlient({ fetch })kabi), testda soxtafetchqo'yamiz.menu-api.test.ts— soxtafetchbilan uch test: to'g'ri javob;narxsatr bo'lgan javob —TypeError;500holat —{ ok: false, status: 500 }.
Ixtiyoriy: haqiqiy API bilan sinab ko'ring — createMenuApi(fetch, "https://ilmhamroh.uz/api/mashq").
Yechim
42-api/menu-api.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 choyturkum !== "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:
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):
✔ 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.9084Haqiqiy 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:
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)
paket.js→paket.ts(git mv),npm run tip— xatolarni sanang va tuzating.paket.tsgaserverRoyxatiniTekshir(malumot: unknown): ServerRoyxati— obyekt,vazifalarmassiv,versiya≥ 0 butun son, har vazifa — import qoidalari bilan.asosiy.tsdagi ikkalaas ServerRoyxatini shu funksiyaga almashtiring.tekshiruv/paket.test.jsga 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:
/** 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:
// 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):
// 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.
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 --mergeCommit 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:
unknownkiradi, tekshirilgan tur chiqadi. - Kod generatsiyasi. Katta jamoalar turlarni qo'lda yozmaydi: server OpenAPI (Swagger) tavsifidan
openapi-typescriptkabi 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?", "anyvaunknownfarqi?", "API javobini TypeScript'da qanday validatsiya qilasiz?".
Xulosa
response.json()vaJSON.parse—any: yozilgan tur tekshiruv emas, va'da. Server o'zgarsa — jim xato.- Tashqi ma'lumot —
unknown:tsctekshiruv yozishga majbur qiladi (TS18046). - Tekshiruv: tur predikati (
value is T) yokiparse(data: unknown): T— xato bo'lsa sababi bilan tashlaydi va faqat kerakli maydonlarni oladi. - Generic klient turni chaqiruvchidan emas,
parsefunksiyasidan olsin.ApiResponse<T>—okmaydonli union: xato holatini unutib bo'lmaydi. - JSON tuzoqlari: sana — satr, katta son,
nullva 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
inOperator" - MDN: "Response.json()", "Response()" — developer.mozilla.org
- typescript-eslint:
no-unsafe-return,no-unsafe-assignment— typescript-eslint.io/rules
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!