8 β Type narrowing va type guard'lar¶
β¬ οΈ Oldingi: 07 β Union va literal tiplar Β· π README Β· Keyingi: 09 β any, unknown, never va void β‘οΈ
Bu bobda: o'tgan bobda
string | numberkabi union (bir nechta tipdan biri) tiplarni yasashni o'rgandik. Endi ulardan foydalanishni o'rganamiz. Union qiymat bilan ishlash uchun TypeScript'ga "hozir aynan qaysi tip ekanini" isbotlash kerak β bu jarayon narrowing (torayish) deyiladi.typeof,instanceof,in, truthiness va tenglik orqali toraytirishni; discriminated union (umumiy tag maydon bo'yicha tarmoqlanish) qolipini;neverbilan exhaustiveness (hamma holat qoplanganmi) tekshiruvini; o'zingizning custom type guardlaringizni (v is X) va assertion function (asserts)larni ko'rib chiqamiz.
Muammo¶
Tasavvur qiling, sizda foydalanuvchining ID'sini qabul qiladigan funksiya bor. Ba'zan ID raqam (42) bo'lib keladi, ba'zan satr ("42") β masalan, URL'dan o'qilganda. JavaScript'da bunday kod yozardingiz:
Bu kod idniKorsat("42") da ishlaydi, lekin idniKorsat(42) da runtime'da qulaydi:
Chunki padStart β faqat satrlarda bor, raqamda yo'q. JavaScript bu xatoni faqat dastur ishga tushganda, foydalanuvchi oldida ushlaydi. TypeScript esa buni kompilyatsiya vaqtida ushlamoqchi. ID'ni union tip qilib belgilaylik:
function idniKorsat(id: string | number) {
return "ID: " + id.padStart(5, "0");
// ^^^^^^^^
// β Xato: Property 'padStart' does not exist on type 'string | number'.
// Property 'padStart' does not exist on type 'number'.
}
TypeScript haq: id raqam ham bo'lishi mumkin, raqamda padStart yo'q. U bizdan avval qaysi tip ekanini aniqlashtirishni talab qilyapti. Aynan shu β narrowing. Quyida uning barcha usullarini ko'rib chiqamiz.
typeof bilan narrowing¶
typeof β JavaScript operatori bo'lib, qiymatning tipini satr ko'rinishida qaytaradi: "string", "number", "boolean", "object", "function", "undefined", "bigint", "symbol". TypeScript typeof tekshiruvini tushunadi va shu shox ichida tipni toraytiradi:
function idniKorsat(id: string | number): string {
if (typeof id === "string") {
// Bu shox ichida TS biladi: id β string
return "ID: " + id.padStart(5, "0"); // β
string'da padStart bor
}
// Bu yerga faqat number bo'lganda yetib keladi
return "ID: " + id.toFixed(0); // β
id endi number
}
if ichida id β string, ifdan keyin esa id β number. TypeScript bu mantiqni avtomatik kuzatadi. Buni control flow analysis (oqim bo'yicha tahlil) deyiladi: TS sizning if/return/throwlaringizni o'qib, har bir nuqtada tip qanaqaligini "biladi".
π Tuzoq: typeof null === "object". Bu JavaScript'ning eski xatosi (afsuski, hech qachon tuzatilmaydi). Shuning uchun nullni typeof bilan ajratib bo'lmaydi β uni alohida === null yoki truthiness bilan tekshiring (pastda ko'ramiz).
π‘ typeof faqat primitiv tiplar uchun ishonchli: string, number, boolean, bigint, symbol, undefined, function. Massiv, sana yoki o'zingizning klassingiz uchun typeof "object" qaytaradi β ularni ajratish uchun boshqa usul kerak.
instanceof bilan narrowing (klasslar)¶
Obyekt qaysi klassdan (yoki konstruktordan) yaratilganini tekshirish uchun instanceof ishlatiladi. Bu klasslar va o'rnatilgan obyektlar (Date, Error, Array...) bilan narrowing'ning asosiy quroli:
function sanani(x: Date | string): string {
if (x instanceof Date) {
// x β Date
return x.toISOString();
}
// x β string
return x.trim();
}
Tez-tez uchraydigan amaliy holat β catch blokidagi xato. Zamonaviy TypeScript'da catch (e) dagi e tipi unknown (noma'lum) bo'ladi, shuning uchun undan to'g'ridan-to'g'ri e.message o'qib bo'lmaydi β avval toraytirish kerak:
function xatoMatni(e: unknown): string {
if (e instanceof Error) {
return e.message; // β
e endi Error, message bor
}
return "Noma'lum xato";
}
π instanceof faqat prototip zanjiri orqali ishlaydi β ya'ni new bilan yaratilgan obyektlar va klasslar uchun. Oddiy { } literal obyekt yoki interface uchun instanceof ishlatib bo'lmaydi (interface'ning runtime'da mavjudligi yo'q). Bunday hollarda in operatori yoki custom type guard kerak bo'ladi.
in operatori bilan narrowing (maydon bormi?)¶
Ikki obyekt tipi farqli maydonlarga ega bo'lsa, qaysi maydon borligiga qarab ajratish mumkin. in operatori "bu obyektda shu nomli maydon bormi?" degan savolga javob beradi:
interface Baliq {
suzadi: () => void;
}
interface Qush {
uchadi: () => void;
}
function harakatlan(jonzot: Baliq | Qush): void {
if ("suzadi" in jonzot) {
jonzot.suzadi(); // β
jonzot β Baliq
} else {
jonzot.uchadi(); // β
jonzot β Qush
}
}
"suzadi" in jonzot rost bo'lsa, TS jonzotni Baliqga toraytiradi; aks holda Qushga. in aynan interface va literal obyekt tiplari uchun qulay β chunki ularda instanceof ishlamaydi.
π‘ in qoladigan maydonni emas, farqlovchi maydonni tekshiring. Agar ikkala tipda ham nom maydoni bo'lsa, "nom" in jonzot hech narsani ajratmaydi. Pastdagi "discriminated union" β bu g'oyani eng toza ko'rinishga keltiradi.
Truthiness va tenglik bilan narrowing¶
if (x) shaklidagi oddiy tekshiruv ham narrowing qiladi. JavaScript'da 0, "", null, undefined, NaN, false β "yolg'on" (falsy), qolgani "rost" (truthy). TypeScript buni biladi va null | undefinedni shu yo'l bilan olib tashlaydi:
function uzunlik(matn: string | null | undefined): number {
if (matn) {
// matn endi shunchaki string (null/undefined chiqib ketdi)
return matn.length;
}
return 0;
}
π Tuzoq: bo'sh satr "" ham falsy. Agar "" to'g'ri qiymat bo'lsa (masalan, foydalanuvchi atayin bo'sh kiritgan), if (matn) uni xato bilan rad etadi. Bunda aniqroq yozing: if (matn !== null && matn !== undefined) yoki qisqasi if (matn != null) β != null ikkala null va undefinedni birato'la ushlaydi.
Tenglik orqali ham narrowing bo'ladi. Ikki union'ni solishtirganda TS ularning umumiy tipini chiqaradi:
function bir(a: string | number, b: string | boolean) {
if (a === b) {
// a === b bo'lsa, ikkalasi ham faqat string bo'la oladi
a.toUpperCase(); // β
a β string
b.toUpperCase(); // β
b β string
}
}
Discriminated union β eng kuchli qolip¶
Union tiplarni ajratishning eng toza, eng kengaytiriladigan usuli β har bir variantga umumiy nomli, lekin literal qiymatli "tag" (yorliq) maydon qo'shish. Bu maydon odatda kind, type yoki tag deb nomlanadi. Quyida geometrik shakllar misoli:
interface Doira {
kind: "doira";
radius: number;
}
interface Tortburchak {
kind: "tortburchak";
tomon: number;
}
interface Uchburchak {
kind: "uchburchak";
asos: number;
balandlik: number;
}
type Shakl = Doira | Tortburchak | Uchburchak;
function yuza(shakl: Shakl): number {
switch (shakl.kind) {
case "doira":
return Math.PI * shakl.radius ** 2; // shakl β Doira
case "tortburchak":
return shakl.tomon ** 2; // shakl β Tortburchak
case "uchburchak":
return (shakl.asos * shakl.balandlik) / 2; // shakl β Uchburchak
}
}
switch (shakl.kind) ichida har bir case o'sha tipga toraytiriladi: case "doira" ichida TS shaklni Doira deb biladi, demak shakl.radius mavjud, lekin shakl.tomon yo'q. Bu shunchaki ishlaydi, chunki kind har variantda literal tip ("doira" β bu o'zining tipi, oddiy string emas).
π‘ Discriminated union β TypeScript'da holatni modellashtirishning eng yaxshi usuli: API javoblari ({ status: "ok", data } | { status: "error", xabar }), Redux/reducer action'lari, formaning holatlari β hammasi shu qolip bilan ifodalanadi. kind maydonini doim literal qiymat qiling, oddiy string emas.
Exhaustiveness β never bilan "hamma holat qoplanganmi?"¶
Yuqoridagi yuza funksiyasi yaxshi, lekin xavf bor: kelajakda Shaklga to'rtinchi shakl β masalan Trapetsiya β qo'shsangiz-u, yuza ichidagi switchga case qo'shishni unutsangiz? Kod jimgina noto'g'ri ishlaydi. TypeScript bizga buni kompilyatsiya vaqtida ushlashga yordam beradi β never tipi orqali.
never β hech qachon yuz bermaydigan qiymat tipi (9-bobda chuqurroq). G'oya: hamma caseni qoplab bo'lgach, defaultga hech narsa qolmasligi kerak. Agar qolsa, demak biz biror variantni unutganmiz:
function yuza(shakl: Shakl): number {
switch (shakl.kind) {
case "doira":
return Math.PI * shakl.radius ** 2;
case "tortburchak":
return shakl.tomon ** 2;
case "uchburchak":
return (shakl.asos * shakl.balandlik) / 2;
default:
// Bu yerga hech qachon yetib kelmasligi kerak.
// Hamma case qoplangan bo'lsa, shakl tipi β never:
const qoldiq: never = shakl;
return qoldiq;
}
}
Hamma variant qoplangan bo'lsa, bu kod toza kompilyatsiya bo'ladi. Endi Shaklga yangi variant qo'shamiz, lekin case qo'shmaymiz:
interface Trapetsiya {
kind: "trapetsiya";
a: number;
b: number;
balandlik: number;
}
type Shakl = Doira | Tortburchak | Uchburchak | Trapetsiya;
function yuza(shakl: Shakl): number {
switch (shakl.kind) {
case "doira":
return Math.PI * shakl.radius ** 2;
case "tortburchak":
return shakl.tomon ** 2;
case "uchburchak":
return (shakl.asos * shakl.balandlik) / 2;
default:
const qoldiq: never = shakl;
// ^^^^^^
// β Xato: Type 'Trapetsiya' is not assignable to type 'never'.
return qoldiq;
}
}
TypeScript shovqin qiladi: defaultga Trapetsiya yetib kelmoqda, lekin biz "hech narsa qolmaydi" deb va'da qilgan edik (never). Bu β ogohlantiruvchi signal: "Trapetsiya uchun case qo'shishni unutding!". Mana shu never hiylasi yirik loyihalarda yangi holat qo'shilganda hech narsani e'tibordan chetda qoldirmaslikni kafolatlaydi.
π‘ Buni yanada toza qilish uchun ko'pincha alohida yordamchi funksiya yoziladi:
function tekshirHammasi(x: never): never {
throw new Error("Qoplanmagan holat: " + JSON.stringify(x));
}
// ... default: return tekshirHammasi(shakl);
Bu ham kompilyatsiyada xato beradi (yangi variant qo'shilsa), ham runtime'da xavfsizlik to'ri vazifasini bajaradi.
Custom type guard β function isX(v): v is X¶
Ba'zan tekshiruv mantig'i murakkab yoki bir necha joyda qayta ishlatiladi. Buni alohida funksiyaga ajratsangiz, TypeScript oddiy boolean qaytaruvchi funksiyadan keyin narrowing qila olmaydi:
function stringmi(x: unknown): boolean {
return typeof x === "string";
}
function ishlat(qiymat: unknown) {
if (stringmi(qiymat)) {
qiymat.toUpperCase();
// β Xato: 'qiymat' is of type 'unknown'.
}
}
TS stringmi rost qaytarsa nima anglatishini bilmaydi β uning uchun bu shunchaki boolean. Hal β qaytuvchi tipni type predicate (x is string) qilib yozish. Bu TypeScript'ga "agar bu funksiya true qaytarsa, argument string deb hisobla" deydi:
function stringmi(x: unknown): x is string {
return typeof x === "string";
}
function ishlat(qiymat: unknown) {
if (stringmi(qiymat)) {
qiymat.toUpperCase(); // β
qiymat endi string
}
}
Endi stringmi(qiymat) rost bo'lgan shoxda qiymat β string. Bu murakkab obyektlar uchun ayniqsa qulay β masalan, API'dan kelgan unknown ma'lumotning kerakli shaklini tekshirish:
interface Foydalanuvchi {
id: number;
ism: string;
}
function foydalanuvchimi(x: unknown): x is Foydalanuvchi {
return (
typeof x === "object" &&
x !== null &&
"id" in x &&
typeof (x as Record<string, unknown>).id === "number" &&
"ism" in x &&
typeof (x as Record<string, unknown>).ism === "string"
);
}
function salomla(data: unknown): string {
if (foydalanuvchimi(data)) {
return "Salom, " + data.ism; // β
data β Foydalanuvchi
}
return "Noma'lum foydalanuvchi";
}
π Type predicate β bu va'da, TypeScript uni tekshirmaydi. Agar x is string deb yozib, ichida noto'g'ri mantiq qo'ysangiz (masalan return typeof x === "number"), TS sizga ishonadi va narrowing'ni noto'g'ri qiladi β runtime'da xato chiqadi. Shuning uchun guard ichidagi mantiq haqiqatan to'g'ri ekaniga o'zingiz javobgarsiz.
π‘ Massivning .filter() metodi bilan birga ishlatganda type guard ayniqsa kuchli: arr.filter((x): x is string => typeof x === "string") natija tipini avtomatik string[] qiladi β oddiy boolean guard bunday qila olmaydi.
Assertion function β asserts¶
Ba'zan tekshiruvni if shoxida emas, balki "shart bajarilmasa, dasturni to'xtat" tarzida yozish qulayroq. Bu uchun assertion function β qaytuv tipi asserts ... bilan boshlanadi. Funksiya xato tashlamasa, undan keyin narrowing kuchga kiradi:
function nullEmasligini(x: unknown, xabar: string): asserts x is NonNullable<typeof x> {
if (x === null || x === undefined) {
throw new Error(xabar);
}
}
function ishlat(matn: string | null) {
nullEmasligini(matn, "matn bo'sh bo'lmasligi kerak");
// Shu qatordan keyin matn β string (null chiqib ketdi)
return matn.toUpperCase(); // β
}
Oddiy asserts shart shakli ham bor β u boolean shartni tekshiradi:
function tasdiqla(shart: unknown, xabar: string): asserts shart {
if (!shart) {
throw new Error(xabar);
}
}
function bol(son: number) {
tasdiqla(son !== 0, "Nolga bo'lib bo'lmaydi");
return 100 / son; // bu yerga faqat son !== 0 bo'lsa yetib keladi
}
π Type guard (v is X) "rost/yolg'on qaytaruvchi savol", assertion (asserts) esa "shart buzilsa, otib tashla". Birinchisi if bilan, ikkinchisi to'g'ridan-to'g'ri chaqiriladi. Ikkalasi ham β sizning va'dangiz: TS ichidagi mantiqning to'g'riligini tekshirmaydi.
Control flow analysis β TS narrowing'ni oqim bo'yicha kuzatadi¶
Eng muhim umumiy g'oya: TypeScript kodingizni yuqoridan pastga, oqim bo'yicha o'qiydi va har bir nuqtada tip nimaga torayganini "eslab qoladi". return, throw, continue orqali shox uzilsa, undan keyingi kod uchun tip avtomatik torayadi:
function birinchiHarf(matn: string | null): string {
if (matn === null) {
return "?";
}
// null shoxi return bilan uzildi -> bu yerda matn faqat string
return matn[0].toUpperCase(); // β
}
Bu early return (erta qaytish) qolipi ham tozaroq kod beradi, ham TS uchun narrowing'ni soddalashtiradi. Aksincha, qiymatga qayta yozish narrowing'ni "buzadi" β TS yangi qiymat tipini qaytadan hisoblaydi:
function aralash(x: string | number) {
if (typeof x === "string") {
x.toUpperCase(); // β
x β string
x = x.length; // endi x ga number yozdik
x.toFixed(2); // β
x β number (TS yangidan hisobladi)
}
}
π‘ Narrowing β sehrli emas, sodda mantiq: TS ko'rgan har bir tekshiruv va o'zlashtirishni hisobga oladi. Kodni qancha tiniq yozsangiz (early return, aniq iflar, discriminated union), narrowing shuncha aniq va xatosiz ishlaydi.
8-bob mashqlari¶
Quyidagi mashqlarni o'zingiz yozib bajaring. Har birini alohida .ts faylda yozib, tsc --noEmit --strict bilan tekshiring. Mashqlar asta-sekin qiyinlashadi.
string | numberqabul qiladiganformatlafunksiyasini yozing: string bo'lsa katta harfga, number bo'lsa ikki kasrli matnga aylantirsin.typeofishlatishni mashq qiling.boolean | numberqabul qiladigan funksiya yozing: boolean bo'lsa"ha"/"yo'q", number bo'lsa o'sha sonni qaytarsin.typeof x === "boolean"ni sinab ko'ring.typeof null === "object"ekanini ko'rsatadigan kod yozing vastring | nulluchuntypeoffaqat string'ni ajrata olishini,nullni alohida tekshirish kerakligini izohlang.Date | stringqabul qiladigan funksiya:Datebo'lsatoISOString(), string bo'lsa o'zini qaytarsin.instanceofishlatishni mashq qiling.catch (e: unknown)blokidaeniinstanceof Errorbilan toraytirib,e.messageni xavfsiz o'qiydigan funksiya yozing.- Ikkita interface (
Mashina { gildirak: number }vaQayiq { eshkak: number }) yarating vainoperatori bilan ularni ajratuvchi funksiya yozing. string | string[]qabul qiladigan funksiya: massiv bo'lsajoin, satr bo'lsa o'zini qaytarsin.Array.isArray()narrowing'ini sinang.string | null | undefineduchun truthiness (if (x)) bilan narrowing qiling. Keyin bo'sh satr""muammosini ko'rsatib,!= nullbilan to'g'rilang.- Uchta variantli discriminated union yarating:
{ kind: "matn", qiymat: string } | { kind: "raqam", qiymat: number } | { kind: "bayroq", qiymat: boolean }.switchbilan har birini chiqaradigan funksiya yozing. - 9-mashqdagi union uchun
switchgadefaultqo'shing vaconst _: never = xbilan exhaustiveness tekshiruvini qo'shing. Toza kompilyatsiya bo'lishini tasdiqlang. - 9-mashqdagi union'ga to'rtinchi variant (
{ kind: "sana", qiymat: Date }) qo'shing, lekincaseqo'shmang.nevertekshiruvi qanday xato berishini ko'ring va xato matnini yozib oling. tekshirHammasi(x: never): neveryordamchi funksiyasini yozing (xato tashlasin) va uni discriminated union'ningdefaultshoxida ishlatib ko'ring.raqammi(x: unknown): x is numbercustom type guard yozing va uniifichida ishlatib,xningnumberga torayishini tekshiring.unknownqiymatni{ id: number; nom: string }shaklidagi obyektga tekshiradiganmahsulotmitype guard yozing (har maydonni alohida tekshiring).Array<string | number>massivini.filter((x): x is string => ...)bilan filtrlab, natija tipistring[]bo'lishini tasdiqlang (natijaga.map(s => s.toUpperCase())qo'llab ko'ring).tasdiqla(shart: unknown, xabar: string): asserts shartassertion funksiyasini yozing va uni nolga bo'lishni taqiqlash uchun ishlating.nullEmasligini(x): asserts x is NonNullable<typeof x>assertion funksiyasini yozing vastring | null | undefinedargument uchun ishlating; chaqiruvdan keyin tipstringga torayishini tekshiring.- Early return qolipi bilan
string | nullargumentni qayta ishlovchi funksiya yozing:nullbo'lsa darrov qaytsin, keyin qolgan kodstringdeb ishlasin. - Bir funksiyada bir nechta narrowing usulini birlashtiring:
string | number | boolean | nullargumentninulltekshiruvi, keyintypeofbilan har bir holatga ajrating va exhaustiveness qo'shing. - Real holat:
{ status: "yuklanmoqda" } | { status: "tayyor"; data: string } | { status: "xato"; xabar: string }discriminated union'ini yarating.switchbilan har bir holatda mos UI matn qaytaring vaneverbilan exhaustiveness'ni kafolatlang. Keyin yangi holat ({ status: "bekor" }) qo'shib, kompilyator sizni qanday ogohlantirishini kuzating.