TypeScript'da haqiqiy type-safety: 6 amaliy pattern
Ko'p loyihalarda TypeScript bor, lekin type-safety yo'q. tsconfig joyida, fayllar .ts, CI'da tsc ham yashil — ammo kodning har uchinchi joyida any, API javoblari as bilan "ishontirilgan", production'da esa baribir undefined is not a function. Men buni "TypeScript yozish" deb atayman. "TypeScript'dan foydalanish" esa boshqa narsa: kompilyatorni xatolarni siz uchun qidiradigan hamkorga aylantirish. Va esda tuting: har safar any yozganingizda siz vaqtdan qarz olasiz — foizini keyinroq production bug'lari va qo'rqinchli refactoring ko'rinishida to'laysiz.
Quyida o'z jamoalarimizda o'zini oqlagan 6 ta pattern. Har biri uchun bir xil format: muammo, yechim kodi va qachon ishlatish kerakligi.
1. Discriminated union: imkonsiz holatlarni imkonsiz qilish
Muammo. So'rov holatini alohida maydonlar bilan ifodalash — keng tarqalgan xato:
interface State {
isLoading: boolean;
data?: User[];
error?: string;
}Bu tip isLoading: true va error: "..." bir vaqtda bo'lishiga ruxsat beradi. Imkonsiz holatlar tipda "yashar" ekan, kod ularni har joyda if bilan himoyalashga majbur — va bir kuni kimdir himoyani unutadi.
Yechim. Holatlarni umumiy status maydoni orqali ajratamiz:
type State =
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; message: string };
function render(state: State) {
switch (state.status) {
case "loading":
return spinner();
case "success":
return list(state.data); // data faqat shu tarmoqda mavjud
case "error":
return alertBox(state.message);
default: {
// yangi status qo'shilsa, bu qator kompilyatsiyada yiqiladi
const _exhaustive: never = state;
return _exhaustive;
}
}
}never orqali exhaustiveness check — eng arzon sug'urta. Kimdir status: "retrying" qo'shsa, kompilyator unutilgan switchlarning har birini o'zi topib beradi — ularni qo'lda qidirib yurishni tasavvur qiling.
Qachon. status, kind, type kabi ajratuvchi maydoni bor har qanday holat: so'rov hayoti (loading/success/error), to'lov bosqichlari, WebSocket xabar turlari, wizard qadamlari.
2. Branded types: string har doim ham shunchaki string emas
Muammo. UserId ham string, OrderId ham string. Funksiyaga argumentlarni almashtirib bersangiz, kompilyator jim turadi — bug esa production'da, boshqa odamning buyurtmasi bekor qilingan payt topiladi.
Yechim. Tipga runtime'da yo'q, faqat kompilyatsiyada yashaydigan "tamg'a" qo'shamiz:
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
// chegarada bir marta belgilab olamiz
const toUserId = (id: string) => id as UserId;
const toOrderId = (id: string) => id as OrderId;
function cancelOrder(orderId: OrderId) { /* ... */ }
const userId = toUserId("u_1042");
cancelOrder(userId); // Xato: 'UserId' tipi 'OrderId' tipiga mos emasE'tibor bering: as faqat bitta joyda — konstruktor-funksiyada. Qolgan butun kod brand'langan tiplar bilan ishlaydi va adashtirishning iloji yo'q.
Qachon. ID'lar, tokenlar, pul (so'm bilan tiyinni adashtirish — real bug!), xom va normallashtirilgan qiymatlar farqi muhim joylar. Ikki xil ma'noli string yoki number bitta funksiyada uchrashsa — brand qiling.
3. satisfies: tekshir, lekin aniq tipni saqlab qol
Muammo. Konfig obyektiga tip berishning eski usullari nimadir yo'qotadi: as umuman tekshirmaydi, oddiy annotatsiya (const c: Config = ...) esa aniq kalitlarni umumiy tipgacha "kengaytirib" yuboradi va autocomplete o'ladi.
Yechim. satisfies shaklga moslikni tekshiradi, lekin toraytirilgan (inferred) tipni qoldiradi:
type ServiceConfig = Record<string, { url: string; timeout: number }>;
// Yomon: "as" hech narsani tekshirmaydi
const legacy = { billing: { url: "https://billing.internal" } } as ServiceConfig;
legacy.billing.timeout; // tipda bor, qiymatda yo'q — runtime'da undefined
// Yaxshi: shakl tekshiriladi, aniq kalitlar saqlanadi
const config = {
billing: { url: "https://billing.internal", timeout: 5000 },
auth: { url: "https://auth.internal", timeout: 3000 },
} satisfies ServiceConfig;
config.auth.timeout; // autocomplete faqat real kalitlarni taklif qiladiFarq oddiy: as — "menga ishon", satisfies — "tekshir, lekin bilganingni unutma".
Qachon. Konfig obyektlari, route xaritalari, i18n lug'atlari, theme'lar — shaklga moslik kerak, lekin kalitlar bo'yicha autocomplete yo'qolmasligi shart bo'lgan har qanday joy.
4. unknown vs any: chegarada ishonch emas, tekshiruv
Muammo. any — tip tizimidan chiqib ketish eshigi va u yuqumli: JSON.parse qaytargan anydan olingan har bir qiymat ham any bo'ladi, xato butun modul bo'ylab tarqaladi. Kompilyator esa hammasiga "OK" deb turaveradi.
Yechim. Chegarada any o'rniga unknown — u qiymatni ishlatishdan oldin tekshirishga majburlaydi:
function parseJson(text: string): unknown {
return JSON.parse(text); // natijani ataylab unknown qilamiz
}
const data = parseJson(rawBody);
// data.email — Xato: 'data' is of type 'unknown'
if (
typeof data === "object" && data !== null &&
"email" in data && typeof data.email === "string"
) {
sendEmail(data.email); // shu blok ichida email aniq string
}any "menga baribir" desa, unknown "avval isbotla" deydi. Butun farq shunda.
Qachon. Har qanday tashqi chegara: JSON.parse, fetch javobi, localStorage, env o'zgaruvchilari, queue'dan kelgan xabar. Qo'lda narrowing zerikarli tuyulsa — keyingi pattern aynan shu haqda.
5. Chegarada runtime validatsiya: zod
Muammo. const user = (await res.json()) as User — bu tip emas, umid. Tiplar runtime'da o'chib ketadi: backend maydon nomini o'zgartirsa yoki null yubora boshlasa, as sizni himoya qilmaydi. "Tipi bor" degani "to'g'ri kelgan" degani emas.
Yechim. Chegarada sxema bilan parse qilamiz — statik tip esa sxemadan o'zi chiqadi:
import { z } from "zod";
const OrderSchema = z.object({
id: z.string(),
amount: z.number().positive(),
status: z.enum(["pending", "paid", "cancelled"]),
});
type Order = z.infer<typeof OrderSchema>; // tip sxemadan hosil bo'ladi
async function fetchOrder(id: string): Promise<Order> {
const res = await fetch(`/api/orders/${id}`);
const result = OrderSchema.safeParse(await res.json());
if (!result.success) {
// kontrakt buzildi — jim davom etmaymiz, aniq xato beramiz
throw new Error(`Order API kontrakti buzildi: ${result.error.message}`);
}
return result.data; // endi bu haqiqatan ham Order
}Bitta sxemadan ham runtime tekshiruv, ham statik tip olasiz — ikkitasini alohida yozib, sinxron ushlab yurish shart emas. valibot yoki ArkType ham xuddi shu ishni qiladi: g'oya muhim, kutubxona emas.
Qachon. Siz nazorat qilmaydigan har qanday kirish: tashqi API, webhook, foydalanuvchi formasi, queue xabari. Ichki funksiyalar orasida shart emas — chegarada bir marta tekshirdingizmi, ichkarida tiplarga bemalol ishonsa bo'ladi.
6. Template literal types: satr formatini tipga aylantirish
Muammo. Event nomlari "user.created" kabi konvensiyaga bo'ysunadi, lekin tipi oddiy string bo'lsa, "user.craeted" xatosi runtime'gacha bemalol yashab yuradi.
Yechim. Formatning o'zini tip qilamiz:
type Entity = "user" | "order" | "payment";
type Action = "created" | "updated" | "deleted";
type EventName = `${Entity}.${Action}`; // 9 ta ruxsat etilgan kombinatsiya
function emit(event: EventName, payload: unknown) { /* ... */ }
emit("payment.created", payload); // OK
emit("payment.done", payload); // Xato: bunday event yo'qYangi entity qo'shilsa, barcha kombinatsiyalar avtomatik paydo bo'ladi — ro'yxatni qo'lda yangilash yo'q.
Qachon. Qisqa, cheklangan formatli satrlar: event nomlari, cache kalit prefikslari, ruxsat nomlari ("orders:read" kabi). Lekin me'yorida ishlating: murakkab satr-parsing tiplari o'qib bo'lmas holga kelsa, bu ham qarzning boshqa ko'rinishi.
Xulosa
Bu 6 pattern'ning umumiy mantig'i bitta: xato qancha erta ushlansa, shuncha arzon. any esa aksincha ishlaydi — xatoni eng qimmat joyga, production'ga surib beradi. Shuning uchun men anyni qarz deb hisoblayman: bugun 5 daqiqa tejaysiz, keyin bir kunlik debug bilan qaytarasiz.
Hammasini birdan joriy qilish shart emas. strict: true yoqilganiga ishonch hosil qiling, chegaralardagi anylarni unknownga almashtiring, eng ko'p og'riq beradigan bitta API'ga zod qo'ying — farqni birinchi haftadayoq sezasiz. TypeScript'ni shunchaki yozmang, undan foydalaning: kompilyator sizga qarshi emas, siz uchun ishlashi kerak.