IlmHamroh
JavaScript Full-stack/15-qism. TypeScript32/47-dars20 daqiqa
Mundarija (31)

TypeScript rekursiv turlar: DeepPartial, JSONValue va kompilyator chegaralari

Qisqacha: Rekursiv tur — o'zini o'zi ishlatadigan tur: JSONValue ichida yana JSONValue[], DeepPartial<T> ichida yana DeepPartial<T[K]>. Ular ichma-ich tuzilmalarni (sozlamalar, JSON, menyu daraxti) tasvirlaydi. Lekin kompilyatorning chegarasi bor: chuqur rekursiya TS2589 ("excessively deep"), juda katta union — TS2590 beradi. Murakkab tur kodni sekinlashtiradi va o'qib bo'lmas qiladi — qayerda to'xtashni bilish ham ko'nikma.

Bu darsda

  • JSONValue ni yozib, qaysi qiymatlar JSON'ga xavfsiz o'tishini turda ifodalaysiz.
  • DeepPartial bilan ichma-ich sozlamalarni qisman yangilaysiz va uning chegaralarini (massiv, Date) bilasiz.
  • Rekursiv conditional type bilan yo'l parametrlarini chiqarasiz: "/menu/:dishId/size/:size" → "dishId" | "size".
  • TS2589 va TS2590 xatolarini tushuntira olasiz va kompilyator chegaralarini raqam bilan bilasiz.
  • Qachon murakkab tur yozmaslik kerakligini ayta olasiz.

Oldin bilishingiz kerak: Branded types va variance, infer kalit so'zi, Rekursiya asoslari.

1. Nega bu kerak?

O'tgan darslarda turlarni boshqa turlardan hisobladik: mapped, conditional, infer, shablon. Hammasi bir qavatli edi. Hayotdagi ma'lumot esa ko'pincha ichma-ich: obyekt ichida obyekt, uning ichida massiv. Bu dars — tur darajasidagi "sehr" qismining oxirgisi va eng chuquri. Uning yarmi — "kutubxona muallifi darajasi", lekin chegaralar haqidagi qismi har bir dasturchiga kerak.

«Bahor» admin panelida sozlamalar bor:

ts
type Settings = {
  hours: { open: number; close: number };
  booking: { from: number; to: number; maxPeople: number };
  theme: "yorug'" | "qorong'i";
};

const settings: Settings = {
  hours: { open: 7, close: 23 },
  booking: { from: 10, to: 22, maxPeople: 12 },
  theme: "yorug'",
};
console.log(settings.booking.maxPeople); // 12

Jasur aka bitta narsani o'zgartirmoqchi: bronda bir stolga ko'pi bilan 8 kishi. Sardor utility types darsidagi Partial ni oldi:

ts
type Settings = {
  hours: { open: number; close: number };
  booking: { from: number; to: number; maxPeople: number };
};

const patch: Partial<Settings> = { booking: { maxPeople: 8 } };

Kompilyator:

text
TS2739: Type '{ maxPeople: number; }' is missing the following properties from type '{ from: number; to: number; maxPeople: number; }': from, to

"{ maxPeople } turida from va to yo'q". Partial faqat birinchi qavatni ixtiyoriy qiladi: booking ni berish shart emas, lekin bersangiz — to'liq. Ichki qavatlarga ham ? qo'yish kerak, ularning ichidagilariga ham — necha qavat bo'lsa, shuncha. Buni bitta tur bilan aytish uchun tur o'zini o'zi chaqirishi kerak.

2. Rekursiv tur: o'zini ishlatadigan tur

2.1 JSONValue

Rekursiv tur (recursive type) — ta'rifida o'zining nomi uchraydigan tur. Bu rekursiv funksiya ning tur darajasidagi egizagi. Kunlik o'xshatish: matryoshka. "Matryoshka — bu qo'g'irchoq, ichida yana matryoshka bo'lishi mumkin". Ta'rif o'zini ishlatadi, lekin oxiri bor — eng kichigi bo'sh.

JSON ma'lumoti aynan shunday tuzilgan. JSON asoslari darsidan: JSON'da satr, son, mantiqiy qiymat, null, massiv va obyekt bo'ladi — massiv va obyekt ichida esa yana shularning istalgani:

ts
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const saved: JSONValue = {
  name: "Osh",
  prices: [35000, 30000],
  tags: { spicy: false, note: null },
};
console.log(JSON.stringify(saved).length); // 72

Kompilyator saved ni tekshirganda turni kerak bo'lgan chuqurlikkacha ochadi:

Qavat Qiymat Qaysi a'zo mos keladi
1 butun obyekt { [key: string]: JSONValue }
2 "Osh" string
2 [35000, 30000] JSONValue[]
3 35000 number
2 { spicy: false, note: null } { [key: string]: JSONValue }
3 false, null boolean, null

Ichma-ichlik qancha chuqur bo'lsa, tekshiruv shuncha qavat tushadi. Qiymat chekli — tekshiruv ham chekli.

2.2 JSON'ga o'tmaydigan narsalar

JSONValue ning foydasi — u JSON'ga jim buziladigan qiymatlarni rad etadi:

ts
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const a: JSONValue = { savedAt: new Date() };
const b: JSONValue = { note: undefined };
const c: JSONValue = { print: () => "osh" };

Kompilyator:

text
TS2322: Type '{ savedAt: Date; }' is not assignable to type 'JSONValue'.
  Types of property 'savedAt' are incompatible.
    Type 'Date' is not assignable to type 'JSONValue | undefined'.
      Type 'Date' is not assignable to type '{ [key: string]: JSONValue; }'.
        Index signature for type 'string' is missing in type 'Date'.
TS2322: Type '{ note: undefined; }' is not assignable to type 'JSONValue'.
  Type '{ note: undefined; }' is not assignable to type 'null'.
TS2322: Type '{ print: () => string; }' is not assignable to type 'JSONValue'.
  Types of property 'print' are incompatible.
    Type '() => string' is not assignable to type 'JSONValue | undefined'.

Har biri haqiqiy JSON tuzog'i (JSON chuqur):

  • Date — JSON.stringify uni satrga aylantiradi, JSON.parse esa Date qilib qaytarmaydi. Zanjirning oxirgi qatori: "Date da satr kalitli indeks imzosi yo'q".
  • undefined — kalit butunlay yo'qoladi. Ikkinchi zanjir qisqa: kompilyator union a'zolarini sinab, oxirgi sinagani (null) bilan xabar berdi.
  • Funksiya — u ham jim tashlab yuboriladi.

2.3 type mos, interface — yo'q

Kutilmagan detal. Xuddi shu shakl interface bilan yozilsa:

ts
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

interface Hours {
  open: number;
  close: number;
}
const hours: Hours = { open: 7, close: 23 };
const json: JSONValue = hours;

Kompilyator:

text
TS2322: Type 'Hours' is not assignable to type 'JSONValue'.
  Type 'Hours' is not assignable to type '{ [key: string]: JSONValue; }'.
    Index signature for type 'string' is missing in type 'Hours'.

Sababi: type alias'ga TypeScript yashirin indeks imzosini beradi, interface ga — yo'q. Interface keyinchalik declaration merging bilan kengayishi mumkin, shuning uchun kompilyator "hamma kalitlar JSONValue" deb kafolat bera olmaydi. Kanon vazifalar loyihasida ham xuddi shu sabab bilan interface VazifaMalumoti dan type ga o'tilgan edi (type va interface qadami, TS2677). Qoida: JSON'ga saqlanadigan ma'lumot shakllarini type bilan yozing.

Tekshirib ko'ring: const x: JSONValue = [1, [2, [3, ["to'rt"]]]]; — tsc o'tkazadimi?

Javob

O'tkazadi. Har qavatda massiv — JSONValue[], uning elementlari — son yoki yana JSONValue[], eng ichidagisi — satr. Chuqurlik qancha bo'lsa ham, har qavat bir xil qoidaga bo'ysunadi.

3. DeepPartial: har qavatni ixtiyoriy qilish

3.1 Yozuv

ts
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };

type Settings = {
  hours: { open: number; close: number };
  booking: { from: number; to: number; maxPeople: number };
};

const patch: DeepPartial<Settings> = { booking: { maxPeople: 8 } };
console.log(patch.booking?.maxPeople); // 8

Mapped type — Partial ning o'zi, faqat qiymat turi T[K] emas, DeepPartial<T[K]>. Kompilyator uni dangasa (lazily) ochadi: faqat kerak bo'lgan qavatni.

Qadam Tur Natija
1 DeepPartial<Settings> { hours?: DeepPartial<…>; booking?: DeepPartial<…> }
2 booking kerak bo'ldi → DeepPartial<{ from; to; maxPeople }> { from?: …; to?: …; maxPeople?: … }
3 maxPeople → DeepPartial<number> number

Uchinchi qadam muhim: DeepPartial<number> — number. Mapped type'ga oddiy (primitiv) tur berilsa, u o'zgarishsiz qaytadi — rekursiya shu yerda to'xtaydi. Bu matryoshkaning "eng kichigi".

3.2 DeepPartial ning chegaralari

Bizning DeepPartial — o'quv nusxasi. Ikki holatda u kutilmagan natija beradi (hover bilan tekshirildi):

Tur DeepPartial natijasi Muammo
{ name: string }[] (DeepPartial<{ name: string }> | undefined)[] massiv elementlari ham undefined bo'la oladi
Date { toString?: …; getTime?: …; … 38 more … } sana — metodlar to'plamiga aylandi

Massivda "elementni qisman yangilash" ma'nosiz: ro'yxat odatda butunligicha almashtiriladi. Date, Map, funksiya — ularni ichidan ochish umuman kerak emas. Kutubxonalardagi tayyor versiyalar (masalan, type-fest dagi PartialDeep) bu holatlarni conditional type bilan alohida ko'radi va 30–60 qatordan iborat. Bu — kutubxona muallifi darajasi: ilova kodida o'zingiz yozmang, tayyorini oling (best practices darsida type-fest haqida).

4. Rekursiv conditional type

4.1 Yo'l parametrlari — barchasi

Template literal types darsida bitta parametrni chiqargan edik va "ko'pi — keyinroq" degandik. Mana o'sha joy:

ts
type Params<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}`
    ? P | Params<Rest>
    : T extends `${string}:${infer P}`
      ? P
      : never;

const key: Params<"/menu/:dishId/size/:size"> = "size";
console.log(key); // size

Params<"/menu/:dishId/size/:size"> — "dishId" | "size". Kompilyator satrni bo'lakma-bo'lak "yeydi":

Qadam T Qaysi shox Natija
1 "/menu/:dishId/size/:size" 1-qolip: P = "dishId", Rest = "size/:size" "dishId" | Params<"size/:size">
2 "size/:size" 1-qolip mos emas (: dan keyin / yo'q), 2-qolip: P = "size" "size"
3 yig'adi — "dishId" | "size"

Bu tur amalda nima beradi? Parametrlar nomidan obyekt turi yasash mumkin — Record bilan:

ts
type Params<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}`
    ? P | Params<Rest>
    : T extends `${string}:${infer P}`
      ? P
      : never;

function buildPath<T extends string>(
  pattern: T,
  params: Record<Params<T>, string>,
): string {
  let path: string = pattern;
  for (const [name, value] of Object.entries<string>(params)) {
    path = path.replace(`:${name}`, value);
  }
  return path;
}

const url = buildPath("/menu/:dishId/size/:size", {
  dishId: "osh",
  size: "katta",
});
console.log(url); // /menu/osh/size/katta

params turi — Record<"dishId" | "size", string>, ya'ni { dishId: string; size: string }. Bitta parametrni unutsangiz yoki nomini xato yozsangiz (dishID), tsc darhol to'xtatadi. Yo'l shablonini o'zgartirsangiz — chaqiruvlar o'zi tekshiriladi. «Bahor» saytidagi har bir havola shunday yasalsa, "sahifa topilmadi" xatolari kamayadi.

Har qadamda satr qisqaradi — rekursiya albatta tugaydi. Bu rekursiv funksiyaning asosiy qoidasi (Rekursiya asoslari): har chaqiruvda masala kichrayishi va to'xtash sharti bo'lishi kerak. Bu yerda to'xtash sharti — oxirgi shox never (parametr qolmadi).

4.2 Siz allaqachon ko'rgan rekursiv tur

infer darsidagi Awaited ham rekursiv: Promise ichida Promise bo'lsa, yana ochadi. Uning soddalashtirilgan ko'rinishi — ichma-ich massivni tekislash:

ts
type Flatten<T> = T extends readonly (infer E)[] ? Flatten<E> : T;

const n: Flatten<number[][][]> = 35000;
console.log(n); // 35000

number[][][] → number[][] → number[] → number: har qadamda bir qavat ochildi, massiv bo'lmay qolganda — to'xtadi.

Tekshirib ko'ring: Params<"/menu"> nima beradi? Nega?

Javob

never. Satrda : yo'q — ikkala qolip ham mos kelmaydi, oxirgi shox never ishlaydi. Bu "parametrsiz yo'l" uchun to'g'ri javob: hech qanday parametr kalit yo'q.

5. Kompilyator chegaralari

Rekursiv turlar kuchli, lekin kompilyator ularni cheksiz hisoblamaydi. Uch chegara bor — ularni TypeScript 7.0.2 da o'zimiz sinab ko'rdik.

5.1 Chuqurlik: TS2589

Satrni N marta takrorlaydigan tur:

ts
type Repeat<
  S extends string,
  N extends number,
  A extends S[] = [],
> = A["length"] extends N ? A : Repeat<S, N, [...A, S]>;

type Ok = Repeat<"osh", 999>;
type TooDeep = Repeat<"osh", 1000>;

Kompilyator:

text
TS2589: Type instantiation is excessively deep and possibly infinite.

Tarjimasi: "Turni hisoblash haddan tashqari chuqur va ehtimol cheksiz". Repeat<"osh", 999> — o'tdi, 1000 — xato. A — har qadamda bitta element qo'shiladigan tuple ("akkumulyator"), uning uzunligi N ga yetganda to'xtaydi. Kichik son bilan, masalan Repeat<"osh", 2>:

Qadam A A["length"] extends 2? Keyingisi
1 [] 0 — yo'q Repeat<"osh", 2, ["osh"]>
2 ["osh"] 1 — yo'q Repeat<"osh", 2, ["osh", "osh"]>
3 ["osh", "osh"] 2 — ha natija: ["osh", "osh"]

N = 1000 da kompilyator shu jadvalning 1000-qatoriga yetolmaydi.

Bu tur "dumli" (tail) rekursiya: rekursiv chaqiruvning natijasi o'zgarishsiz qaytariladi (? A : Repeat<…>). Bunday turlar uchun TypeScript 4.5 dan beri chegara — 1000 qadam. Natija ustiga nimadir qo'shilsa (masalan, 1 | Len<R>), chegara ancha past. Bizning sinovda bunday tur 48 qavatda ishladi, 49 da — xuddi shu TS2589.

Rekursiya turi Misol Biz o'lchagan chegara
dumli ? A : Repeat<S, N, [...A, S]> 999 — o'tdi, 1000 — TS2589
dumsiz ? 1 | Len<R> : 0 48 — o'tdi, 49 — TS2589

Cheksiz rekursiya ham shu xato bilan to'xtaydi — kompilyator "osilib" qolmaydi.

5.2 Kenglik: TS2590

Template literal types darsida juftliklar ko'paytma bilan o'sishini ko'rdik:

ts
type D = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type FiveDigits = `${D}${D}${D}${D}${D}`;

Kompilyator:

text
TS2590: Expression produces a union type that is too complex to represent.

"Ifoda ko'rsatib bo'lmaydigan darajada murakkab union turini hosil qiladi". Besh xonali kodlar — 100 000 ta satr. To'rt xonali (10 000 ta) — hali o'tadi.

5.3 Narx: sekinlik

Xato bermagan tur ham bepul emas. Biz to'rt xil o'lchamdagi kod union'ini va undan mapped type bilan yasalgan ishlovchilar turini tekshirib, tsc --extendedDiagnostics hisobotidan kompilyator yaratgan turlar sonini oldik:

Union o'lchami va kompilyator yaratgan turlar
  • 10 ta kodtekshiruv ~0,04 s7 856 ta tur
  • 100 ta kod~0,04 s8 047 ta tur
  • 1 000 ta kod~0,05 s9 947 ta tur
  • 10 000 ta kod~0,12 s, xotira +50 %28 947 ta tur

Manba: O'lchandi: tsc 7.0.2 --extendedDiagnostics (Types, Check time — 7 marta medianasi), Windows 11, 2026-10-06

Taxminan 7 800 tasi — standart kutubxona turlari (har faylda bor). 1 000 tagacha farq sezilmaydi. 10 000 da — turlar uch barobar, tekshiruv vaqti ham taxminan uch barobar. Bitta fayldagi bitta tur uchun 0,1 soniya kam ko'rinadi. Lekin muharrir har tugma bosilganda qayta tekshiradi, katta loyihada esa bunday turlar o'nlab. Natija — sekin avtoto'ldirish va kechikkan qizil chiziqlar.

6. Qachon to'xtash kerak

Tur darajasidagi dasturlash o'ziga tortadi: "buni ham turda ifodalasam-chi?". Mana amaliy qoidalar:

Avval bir tamoyil: tur — kod o'quvchisi uchun hujjat ham. Uni siz bir marta yozasiz, boshqalar esa yuz marta o'qiydi: hover'da, xato xabarida, kod ko'rib chiqishda. Murakkab tur yozish vaqtingizni emas, ularning vaqtini sarflaydi.

  1. Tur ma'lumotni tasvirlasin, hisob-kitob qilmasin. JSONValue, DeepPartial, Params — ma'lumot shakli. Tur ichida qo'shish, saralash, satrni parse qilish — o'yin, ish emas.
  2. Hover'da o'qib bo'lmasa — juda murakkab. Hamkasbingiz xato xabarini tushunmasa, tur unga yordam bermayapti.
  3. Tayyorini oling. type-fest, kutubxonalarning o'z turlari — sinalgan, chegaraviy holatlari hal qilingan.
  4. Chegara yaqinida — boshqa yo'l. TS2589 yoki TS2590 ni ko'rsangiz, odatda yechim — turni soddalashtirish, // @ts-ignore emas. (// @ts-ignore — "keyingi qatordagi xatoni ko'rsatma" degan izoh; uni JavaScript'dan TypeScript'ga ko'chish darsida ko'ramiz. U muammoni yashiradi, hal qilmaydi.)
  5. Tur — tekshiruv o'rnini bosmaydi. Keyingi bo'lim — shu haqda.

7. TypeScript ushlamaydigan tuzoq: JSONValue ham NaN ni o'tkazadi

JSONValue Date va undefined ni ushladi. Lekin uning number a'zosi hamma sonlarni qabul qiladi:

ts
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const settings: JSONValue = { price: NaN };
const text = JSON.stringify(settings);
const back = JSON.parse(text) as { price: number };
console.log(back.price.toFixed(0));

tsc — xatosiz. Node esa:

text
TypeError: Cannot read properties of null (reading 'toFixed')

JSON.stringify NaN va Infinity ni null ga aylantiradi — JSON'da ular yo'q. Turda esa NaN — number, number esa JSONValue ga kiradi. TypeScript'da "chekli son" degan tur yo'q: NaN ni turlar bilan ajratib bo'lmaydi. as { price: number } esa qaytgan qiymatni tekshirmasdan qabul qildi.

Himoya: saqlashdan oldin Number.isFinite bilan tekshiring; o'qishda esa JSON.parse natijasini unknown deb oling va shaklini tekshiring (API ma'lumotlarini tiplash darsida to'liq).

8. Ko'p uchraydigan xatolar

8.1 Ichma-ich obyektga Partial

TS2739: … is missing the following properties …: from, to — Partial bir qavatli. Tuzatish: DeepPartial (yoki ichki turni alohida Partial qiling).

8.2 JSON ma'lumotini interface bilan yozish

Index signature for type 'string' is missing in type 'Hours'. Tuzatish: type Hours = { … }.

8.3 To'xtash sharti yo'q rekursiya

TS2589: Type instantiation is excessively deep and possibly infinite. Har qadamda tur kichraymasa yoki oxirgi shox bo'lmasa — cheksiz. Tuzatish: har chaqiruvda nima kichrayishini tekshiring (satr qisqaradimi, tuple'ga element qo'shiladimi).

8.4 Juda katta union

TS2590: Expression produces a union type that is too complex to represent. Tuzatish: kombinatsiyani turga emas, ma'lumotga qo'ying yoki ${number} kabi naqsh turini ishlating.

8.5 DeepPartial<Date>

Sana metodlar to'plamiga aylanadi. Tuzatish: sanani sozlamalarda satr ("2026-10-06") yoki son sifatida saqlang yoki tayyor PartialDeep ni oling.

9. Mashqlar

1-mashq (oson): Qavatlarni sanang

ts
type Flatten<T> = T extends readonly (infer E)[] ? Flatten<E> : T;
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };

const a: Flatten<string[][]> = "osh";
const b: DeepPartial<{ hours: { open: number } }> = { hours: {} };
console.log(a, Object.keys(b)); // osh [ 'hours' ]

Flatten<string[][]> — [:string]. Flatten bu yerda o'zini necha marta chaqirdi (birinchi chaqiruvdan tashqari)? [:2]. DeepPartial<number> — .

Yechim

string[][] → Flatten<string[]> (1-chaqiruv) → Flatten<string> (2-chaqiruv) → string massiv emas, to'xtaydi. DeepPartial primitiv turni o'zgartirmaydi — number. { hours: {} } ham to'g'ri: hours ichidagi open ham ixtiyoriy.

2-mashq (o'rta): DeepReadonly

DeepReadonly<T> yozing: har qavatdagi har maydon readonly. Sozlamalar obyektida settings.booking.maxPeople = 20 xato bersin. Ishora: DeepPartial dagi ? o'rniga oldiga readonly.

Yechim
ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: DeepReadonly<T[K]>;
};

type Settings = {
  booking: { from: number; to: number; maxPeople: number };
};

const settings: DeepReadonly<Settings> = {
  booking: { from: 10, to: 22, maxPeople: 12 },
};
settings.booking.maxPeople = 20;

Kompilyator:

text
TS2540: Cannot assign to 'maxPeople' because it is a read-only property.

Tarjimasi: "maxPeople ga yozib bo'lmaydi, chunki u faqat o'qish uchun" — ikkinchi qavatda ham. Oddiy Readonly<Settings> faqat settings.booking = … ni taqiqlardi, settings.booking.maxPeople = 20 esa o'tib ketardi. Eslatma: bu ham faqat tur — ishlash paytida himoya uchun Object.freeze ni har qavatga qo'llash kerak.

3-mashq (qiyin): Amaliy tajriba — sozlamalarni yangilash

kurs/mashqlar/15/32-recursive/settings.ts faylini yarating:

  • JSONValue, DeepPartial<T> — darsdagidek.
  • Settings (type bilan!) va DEFAULTS — «Nega bu kerak?» bo'limidagi qiymatlar.
  • applySettings(base, patch) — patch: DeepPartial<Settings>, yangi Settings qaytaradi; asl obyekt o'zgarmaydi; undefined qiymat eskisini o'chirmaydi (Mapped types dagi tuzoq!).
  • toJSON(value: JSONValue) — JSON.stringify ning tur bilan himoyalangan o'rami.

Ishora: ichma-ich birlashtirishni turlarsiz yordamchi funksiya qiladi — mergeDeep(base: unknown, patch: unknown): unknown. Ikkalasi ham oddiy obyekt bo'lsa — kalitma-kalit o'zini chaqiradi, aks holda — patch (u undefined bo'lsa — base). applySettings uning natijasini as Settings bilan qaytaradi — bu "tekshiruvsiz joy", uni testlar himoya qiladi.

Avval applySettings parametrini Partial<Settings> qilib, testlarni yozing va npx tsc --pretty false ni ishga tushiring.

Yechim

Partial bilan testlardagi ikki chaqiruv to'xtadi:

text
32-recursive/settings.test.ts(7,42): error TS2739: Type '{ maxPeople: number; }' is missing the following properties from type '{ from: number; to: number; maxPeople: number; }': from, to
32-recursive/settings.test.ts(13,29): error TS2741: Property 'open' is missing in type '{ close: number; }' but required in type '{ open: number; close: number; }'.
ts
// 32-recursive/settings.ts — «Bahor» sozlamalari
export type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };

export type Settings = {
  hours: { open: number; close: number };
  booking: { from: number; to: number; maxPeople: number };
  theme: "yorug'" | "qorong'i";
};

export const DEFAULTS: Settings = {
  hours: { open: 7, close: 23 },
  booking: { from: 10, to: 22, maxPeople: 12 },
  theme: "yorug'",
};

function isPlainObject(
  value: unknown,
): value is Record<string, unknown> {
  if (typeof value !== "object" || value === null) return false;
  return !Array.isArray(value);
}

// Ichma-ich obyektlarni birlashtiradi; undefined qiymat — e'tiborsiz
function mergeDeep(base: unknown, patch: unknown): unknown {
  if (!isPlainObject(base) || !isPlainObject(patch)) {
    return patch === undefined ? base : patch;
  }
  const result: Record<string, unknown> = { ...base };
  for (const [key, value] of Object.entries(patch)) {
    result[key] = mergeDeep(base[key], value);
  }
  return result;
}

export function applySettings(
  base: Settings,
  patch: DeepPartial<Settings>,
): Settings {
  // mergeDeep turlarni bilmaydi — natija shaklini testlar tekshiradi
  return mergeDeep(base, patch) as Settings;
}

export function toJSON(value: JSONValue): string {
  return JSON.stringify(value);
}
  • mergeDeep — oddiy rekursiv funksiya (Rekursiya asoslari), DeepPartial — uning tur darajasidagi egizagi. Ikkalasi bir xil chuqurlikka tushadi.
  • { ...base } — har qavatda yangi obyekt: asl DEFAULTS o'zgarmaydi.
  • toJSON(DEFAULTS) tsc dan o'tadi, chunki Settings — type. interface bo'lganda — «type mos, interface — yo'q» bo'limidagi xato.
ts
// 32-recursive/settings.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { DEFAULTS, applySettings, toJSON } from "./settings.ts";

test("applySettings: faqat berilgan maydon o'zgaradi", () => {
  const next = applySettings(DEFAULTS, { booking: { maxPeople: 8 } });
  assert.deepEqual(next.booking, { from: 10, to: 22, maxPeople: 8 });
  assert.deepEqual(next.hours, DEFAULTS.hours);
});

test("applySettings: asl sozlama o'zgarmaydi", () => {
  applySettings(DEFAULTS, { hours: { close: 22 } });
  assert.equal(DEFAULTS.hours.close, 23);
});

test("applySettings: undefined eski qiymatni o'chirmaydi", () => {
  const next = applySettings(DEFAULTS, { theme: undefined });
  assert.equal(next.theme, "yorug'");
});

test("toJSON: sozlama JSON'ga aylanadi va qaytadi", () => {
  const text = toJSON(DEFAULTS);
  assert.deepEqual(JSON.parse(text), DEFAULTS);
});

DeepPartial ga qaytarilgach npx tsc --pretty false — chiqishsiz. node --test 32-recursive/settings.test.ts bizda (Node 24.21.0):

text
✔ applySettings: faqat berilgan maydon o'zgaradi (2.9562ms)
✔ applySettings: asl sozlama o'zgarmaydi (0.3829ms)
✔ applySettings: undefined eski qiymatni o'chirmaydi (0.3098ms)
✔ toJSON: sozlama JSON'ga aylanadi va qaytadi (0.6491ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 269.6082

Uchinchi test — Mapped types darsidagi Partial + spread tuzog'ining himoyasi: oddiy { ...base, ...patch } bo'lganda theme undefined bo'lib qolardi.

XATOLAR.md ga qo'shing: TS2589 (rekursiya chuqur), TS2590 (union juda katta), TS2739 (Partial bir qavatli), "Index signature … is missing" (interface va JSONValue). Keyin kurs/mashqlar papkasidan:

bash
git add 15/32-recursive 15/XATOLAR.md
git commit -m "15/32: settings.ts — DeepPartial va JSONValue"

10. Real ishda

  • JSON va API turlari: JSONValue (yoki kutubxonalardagi JsonValue) — localStorage, postMessage, server javoblari uchun. Ko'p loyihalarda u birinchi yoziladigan rekursiv tur.
  • Sozlamalar va forma qoralamalari: DeepPartial — test ma'lumotlari ("faqat shu maydonni o'zgartirib, qolgani standart"), sozlamalarni qisman yangilash, ichma-ich formalar.
  • Daraxtlar: menyu kategoriyalari, izohlar va javoblar, fayl tizimi — type Category = { name: string; children: Category[] }. Daraxtlar bilan 14-qismda ishlagansiz — endi ularning turini ham yozasiz.
  • Kutubxona muallifi darajasi: yo'l parametrlarini chiqarish (Express, Next.js turlari), SQL so'rovidan natija turini hisoblash (Drizzle, Kysely kabi kutubxonalar) — chuqur rekursiv turlar. Ularning muallifi TS2589 bilan doimiy kurashadi. Siz esa ularni foydalanuvchi sifatida ishlatasiz.
  • Intervyu: "DeepPartial ni yozing", "JSONValue turini yozing", "Type instantiation is excessively deep xatosini qanday tuzatasiz?" — yuqori darajadagi savollar.

Xulosa

  • Rekursiv tur — ta'rifida o'zi uchraydigan tur; ichma-ich ma'lumot (JSON, sozlamalar, daraxt) uchun.
  • JSONValue JSON'ga jim buziladigan Date, undefined, funksiyani ushlaydi; NaN ni esa ushlamaydi. JSON shakllari — type bilan.
  • DeepPartial — Partial ning har qavatga tushadigan versiyasi; primitivda to'xtaydi; massiv va Date da g'alati — tayyor PartialDeep ni oling.
  • Rekursiv conditional type — satr yoki tuple'ni bo'lakma-bo'lak "yeydi"; har qadamda kichrayishi shart.
  • Chegaralar (TypeScript 7.0.2 da o'lchandi): dumli rekursiya ~1000 qadam, dumsiz ~50, union — 100 000 dan kam. Murakkab tur muharrirni sekinlashtiradi — tur ma'lumotni tasvirlasin, hisoblamasin.

Keyingi dars: tsconfig.json chuqur — tur darajasidagi "sehr" tugadi. Endi kompilyatorni sozlaymiz: target, lib, module, moduleResolution va TypeScript 6/7 ning yangi standartlari.

Manbalar

  • TypeScript Handbook: "Conditional Types" va "Mapped Types" — typescriptlang.org/docs/handbook/2/types-from-types.html
  • TypeScript Release Notes 3.7 (recursive type aliases), 4.1 (recursive conditional types), 4.5 (tail-recursion elimination on conditional types) — typescriptlang.org/docs/handbook/release-notes/
  • TypeScript Wiki: "Performance" (murakkab turlar va kompilyator tezligi) — github.com/microsoft/TypeScript/wiki/Performance
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
TypeScript rekursiv turlar: DeepPartial, JSONValue va kompilyator chegaralari — IlmHamroh