IlmHamroh
JavaScript Full-stack/18-qism. React chuqur37/42-dars20 daqiqa
Mundarija (28)

React + TS IV: generic, polymorphic va discriminated union props — xatoni kompilyatsiyada ushlash

Qisqacha: Generic komponent (function List<T>(props: ListProps<T>)) bir xil kodni har xil ma'lumot turi bilan ishlatadi va turni yo'qotmaydi: items — taomlar bo'lsa, renderItem ham taom oladi. Discriminated union props esa "yo bu, yo u" qoidasini turga yozadi: { kind: "fixed"; amount } | { kind: "range"; min; max }. Ajratuvchi maydon bo'lmasa — ?: never bilan istisno: { children } | { children?: never; "aria-label": string }. Polymorphic komponent as prop'iga qarab boshqa teg bo'ladi va atributlarni shu tegga mos tekshiradi. satisfies esa obyektni turga solishtiradi, lekin uning aniq turini saqlab qoladi.

Bu darsda

  • Generic komponent yozasiz va .tsx dagi <T,> tuzog'ini bilasiz.
  • Bir-birini istisno qiladigan props'ni discriminated union va never bilan yozasiz.
  • Destrukturizatsiya qilinganda union nega "buziladi"ni ko'rasiz.
  • Polymorphic as prop'ini turlab, <Text as="a" href> ni to'g'ri, <Text as="span" href> ni xato qilasiz.
  • vazifalar dagi Tugma ga "nomi bo'lishi shart" qoidasini tur bilan qo'shasiz.

Oldin bilishingiz kerak: React + TS III, Generics, Discriminated union, Polymorphic komponentlar.

1. Nega bu kerak?

Oldingi darsda ComponentProps<"button"> bilan tugmani "standart rozetka" qildik. Endi u har qanday props'ni qabul qiladi — hatto ma'nosiz kombinatsiyalarni ham. Ikki voqea:

  • Sardor menyuga yopish tugmasini qo'shdi: <Tugma onClick={yop}><XIcon /></Tugma>. Ichida faqat ikonka, matn yo'q. Ko'zi ko'radigan foydalanuvchi "" ni tushunadi. Malika esa ekran o'quvchida "tugma" degan bitta so'zni eshitadi. Qaysi tugma, nima qiladi — noma'lum.
  • Jasur aka narx komponentiga amount ham, min/max ham berdi. Komponent qaysi birini ko'rsatishni bilmay, birinchisini tanladi. Menyuda noto'g'ri narx chiqdi.

Ikkala xato ham ish vaqtida, foydalanuvchida topildi. Bu darsda ularni tsc topadi.

Hayotiy o'xshatish: poyezd chiptasi. Chiptada yo "o'tirish joyi", yo "yotish joyi" bo'ladi — ikkalasi bir vaqtda emas, hech biri yo'q ham emas. Kassa dasturi bunday chiptani chiqarishga yo'l qo'ymaydi. Union props — kassaning shu qoidasi.

2. Generic komponentlar

2.1 Muammo: unknown yoki takrorlash

«Bahor» da ro'yxatlar ko'p: taomlar, stollar, buyurtmalar. Har biri uchun alohida DishList, TableList yozish — takrorlash. Bitta List yozib, items: unknown[] desak — tur yo'qoladi: renderItem ichida item.name xato bo'ladi. Generics darsidagi yechim komponentga ham ishlaydi:

tsx
import type { ReactNode } from "react";

type ListProps<T> = {
  items: readonly T[];
  getKey: (item: T) => string | number;
  renderItem: (item: T) => ReactNode;
};

function List<T>({ items, getKey, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={getKey(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

type Dish = { id: number; name: string; price: number };
const dishes: Dish[] = [
  { id: 1, name: "Osh", price: 35000 },
  { id: 4, name: "Manti", price: 30000 },
];

export default function App() {
  return (
    <div>
      <List
        items={dishes}
        getKey={(d) => d.id}
        renderItem={(d) => `${d.name} — ${d.price} so'm`}
      />
      <List
        items={[5, 7]}
        getKey={(n) => n}
        renderItem={(n) => `${n}-stol bo'sh`}
      />
    </div>
  );
}

Ekranda:

text
Osh — 35000 so'm
Manti — 30000 so'm
5-stol bo'sh
7-stol bo'sh

<List items={dishes} …> da T ni hech kim yozmadi. TypeScript uni items dan chiqardi: T = Dish. Shundan keyin getKey={(d) => …} va renderItem={(d) => …} dagi d ham Dish — d.name xatosiz, d.nmae esa xato. Ikkinchi ro'yxatda T = number. Bir komponent — ikki xil, lekin har biri to'liq turlangan.

Generic komponentni React.FC bilan yozib bo'lmasligini React + TS I da aytgan edik: const List: FC<ListProps<T>> — T qayerdan keladi? Oddiy function bunda tabiiy.

2.2 Cheklov: T extends …

Komponent T haqida biror narsani bilishi kerak bo'lsa — cheklov. Masalan, getKey ni har safar berish zerikarli, ro'yxat elementida baribir id bor:

tsx
type ListProps<T extends { id: number }> = {
  items: readonly T[];
  renderItem: (item: T) => string;
};

function List<T extends { id: number }>({
  items,
  renderItem,
}: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

export default function App() {
  const tables = [{ id: 5, seats: 4 }, { id: 7, seats: 2 }];
  return (
    <List
      items={tables}
      renderItem={(t) => `${t.id}: ${t.seats} o'rin`}
    />
  );
}

Ekranda:

text
5: 4 o'rin
7: 2 o'rin

item.id — komponent ichida xatosiz, chunki cheklov kafolatlaydi. t.seats esa tashqarida ham bor — T toraymadi, faqat "kamida id bor" deyildi. id siz massiv bersangiz (items={["Osh"]}) — tsc xatosi.

2.3 .tsx dagi <T,> tuzog'i

Generic funksiyani strelka bilan yozmoqchi bo'lsangiz, .tsx da kutilmagan xato chiqadi:

tsx
const first = <T>(items: T[]) => items[0];
console.log(first([1]));

Xato:

text
TS17008: JSX element 'T' has no corresponding closing tag.
TS1382: Unexpected token. Did you mean `{'>'}` or `&gt;`?
TS1005: '</' expected.

"JSX elementi T ning yopuvchi tegi yo'q." .tsx faylda <T> — JSX tegi boshlanishi bilan bir xil ko'rinadi, tahlilchi uni <div> kabi o'qidi. Yechimlar:

  • <T,> — vergul "bu teg emas, tur parametrlari ro'yxati" deb ko'rsatadi: const first = <T,>(items: T[]) => items[0];
  • <T extends unknown> — xuddi shu ta'sir.
  • Eng oddiysi — function first<T>(items: T[]) { … }. Bu kursda shunday.

2.4 Tur argumentini ochiq berish

Ba'zan chiqarish kerakli turni bermaydi — masalan, boshlang'ich ro'yxat bo'sh. JSX'da ham tur argumentini yozish mumkin:

tsx
<List<Dish>
  items={[]}
  getKey={(d) => d.id}
  renderItem={(d) => d.name}
/>

Bo'sh [] dan T ni chiqarib bo'lmaydi — <List<Dish> uni aniq aytadi. Bu useState<string[]>([]) dagi muammoning JSX'dagi ko'rinishi.

Tekshirib ko'ring: List ning renderItem turi (item: T) => ReactNode o'rniga (item: unknown) => ReactNode bo'lsa, renderItem={(d) => d.name} da nima bo'ladi?

Javob

d turi unknown bo'ladi va d.name — TS18046: 'd' is of type 'unknown'. items va renderItem orasidagi bog'lanish uzildi: TypeScript renderItem ga aynan items dagi element tushishini bilmaydi. Generic T aynan shu bog'lanishni ifodalaydi: "kirgan tur — chiqqan tur".

3. Discriminated union props

3.1 Ajratuvchi maydon bilan

Jasur akaning narx komponenti. Narx — yo aniq (35000), yo oraliq (28000–35000, porsiyaga qarab). Ikkalasi bir vaqtda ma'nosiz:

tsx
type PriceProps =
  | { kind: "fixed"; amount: number }
  | { kind: "range"; min: number; max: number };

function Price(props: PriceProps) {
  if (props.kind === "fixed") {
    return <p>{props.amount} so'm</p>;
  }
  return (
    <p>
      {props.min}–{props.max} so'm
    </p>
  );
}

export default function App() {
  return (
    <div>
      <Price kind="fixed" amount={35000} />
      <Price kind="range" min={28000} max={35000} />
    </div>
  );
}

Ekranda:

text
35000 so'm
28000–35000 so'm

Bu — Discriminated union darsidagi naqsh, faqat props'da. kind — ajratuvchi (discriminant). props.kind === "fixed" dan keyin props.amount bor, props.min yo'q. Endi Jasur akaning xatosi:

tsx
type PriceProps =
  | { kind: "fixed"; amount: number }
  | { kind: "range"; min: number; max: number };

function Price(props: PriceProps) {
  return <p>{props.kind}</p>;
}

export default function App() {
  return (
    <div>
      <Price kind="fixed" min={28000} />
      <Price kind="range" min={28000} max={35000} amount={30000} />
    </div>
  );
}

Xato:

text
TS2322: Type '{ kind: "fixed"; min: number; }' is not assignable to type 'IntrinsicAttributes & PriceProps'.
  Property 'min' does not exist on type 'IntrinsicAttributes & { kind: "fixed"; amount: number; }'.
TS2322: Type '{ kind: "range"; min: number; max: number; amount: number; }' is not assignable to type 'IntrinsicAttributes & PriceProps'.
  Property 'amount' does not exist on type 'IntrinsicAttributes & { kind: "range"; min: number; max: number; }'.

Xabarlarga qarang: tsc kind ga qarab qaysi variant ekanini tanladi va aynan shu variantga solishtirdi. kind="fixed" — min yo'q. kind="range" — amount yo'q. Noto'g'ri narx endi menyuga chiqmaydi — build'dan o'tmaydi.

3.2 Tuzoq: destrukturizatsiya union'ni "buzadi"

Props'ni har doim destrukturizatsiya qilib o'rgangansiz. Union props bilan bu xavfli:

tsx
type PriceProps =
  | { kind: "fixed"; amount: number }
  | { kind: "range"; min: number; max: number };

function Price({ kind, ...rest }: PriceProps) {
  if (kind === "fixed") {
    return <p>{rest.amount} so'm</p>;
  }
  return <p>{rest.min} so'mdan</p>;
}

export default function App() {
  return <Price kind="fixed" amount={35000} />;
}

Xato:

text
TS2339: Property 'amount' does not exist on type '{ amount: number; } | { min: number; max: number; }'.
  Property 'amount' does not exist on type '{ min: number; max: number; }'.
TS2339: Property 'min' does not exist on type '{ amount: number; } | { min: number; max: number; }'.
  Property 'min' does not exist on type '{ amount: number; }'.

kind va rest — endi ikki alohida o'zgaruvchi. kind === "fixed" tekshiruvi kind ni toraytiradi, lekin rest haqida TypeScript hech narsa bilmaydi. Bog'lanish uzildi. Qoida: union props'ni avval butun holida (props) tekshiring, keyin tarmoq ichida destrukturizatsiya qiling. TypeScript faqat bitta holatni kuzatadi: barcha xususiyat oddiy const ga yechilsa ({ kind, amount } — rest siz), u ularni bog'liq deb biladi. ...rest bilan — yo'q.

3.3 Ajratuvchisiz: ?: never bilan istisno

Ba'zan ajratuvchi maydon yo'q va qo'shib bo'lmaydi. Sardorning tugmasi: nomi yo matn (children) bilan, yo aria-label bilan. kind="matnli" deb yozish ortiqcha. Bunday union'da "bu variantda bu xususiyat bo'lmasin" qoidasini never ifodalaydi:

tsx
type Name =
  | { children: ReactNode; "aria-label"?: string } // matnli
  | { children?: never; "aria-label": string }; // faqat ikonka

Ikkinchi variantni o'qing: "children berilsa, turi never bo'lishi kerak". never turidagi qiymat yo'q — demak children umuman berilmasligi kerak. Va aria-label majburiy. Natija jadvalda:

Berilgan 1-variant 2-variant Natija
children (never) o'tadi
aria-label (children yo'q) o'tadi
ikkalasi o'tadi
hech biri TS2322

Ikkalasi berilganda ham o'tadi: matnli tugmaga qo'shimcha aria-label ba'zan kerak (masalan, " Yopish" matni bor, lekin ekran o'quvchiga "Savatni yopish" aniqroq). Faqat "hech biri" — taqiqlangan.

flowchart TD
  A["&lt;Tugma … /&gt;"] --> B{"children bormi?"}
  B -- "ha" --> C["1-variant: o'tadi"]
  B -- "yo'q" --> D{"aria-label bormi?"}
  D -- "ha" --> E["2-variant: o'tadi"]
  D -- "yo'q" --> F["TS2322: nomsiz tugma"]

Diagrammada TS2322 ga olib boradigan yagona yo'l — Malika eshitadigan "nomsiz tugma". Endi u build'da to'xtaydi. Haqiqiy xabarni «Vazifalar qadami» bo'limida, vazifalar dagi o'lchovda ko'ramiz.

Tekshirib ko'ring: Nega 2-variantda children?: never yozildi? Shunchaki { "aria-label": string } bo'lsa nima o'zgaradi?

Javob

Union "ochiq": { "aria-label": string } varianti children haqida hech narsa demaydi. TypeScript'ning ortiqcha xususiyat tekshiruvi union'da yumshoqroq ishlaydi — u boshqa variantlardagi kalitlarni "tanish" deb biladi. Natijada noto'g'ri kombinatsiyalar o'tib ketishi va narrowing ("children" in props) chalkash bo'lishi mumkin. children?: never qoidani ochiq yozadi: bu variantda children yo'q. Bu ikki variantni bir-biridan aniq ajratadi.

4. Polymorphic komponentlar: as prop

4.1 G'oya

Polymorphic komponentlar darsida as prop'ini ko'rgan edik: <Text as="h2"> — sarlavha, <Text as="a" href> — havola, ko'rinishi bir xil. U yerda asosiy savol "nega kerak" edi va turning birinchi, qisqa variantini ko'rdik (ElementType + ComponentPropsWithoutRef<T>). Bugun uni generic bilimi bilan oxirigacha o'qiymiz: standart teg, children va xato xabarining har qismi. Maqsad o'sha: as="a" bo'lsa, href ruxsat etilsin, as="span" bo'lsa — yo'q.

4.2 Turi

as ning qiymati — teg nomi yoki komponent. Bu tur react da bor: ElementType. Props esa shu tegning props'i — ComponentProps<E>:

tsx
import type { ComponentProps, ElementType, ReactNode } from "react";

type TextProps<E extends ElementType> = {
  as?: E;
  children: ReactNode;
} & Omit<ComponentProps<E>, "as" | "children">;

function Text<E extends ElementType = "span">({
  as,
  ...rest
}: TextProps<E>) {
  const Tag = as ?? "span";
  return <Tag className="matn" {...rest} />;
}

export default function App() {
  return (
    <div>
      <Text as="h2">Bugungi menyu</Text>
      <Text as="a" href="https://example.com/menyu">
        To'liq menyu
      </Text>{" "}
      <Text>yangilangan</Text>
    </div>
  );
}

Ekranda:

text
Bugungi menyu
To'liq menyu yangilangan

Qadamma-qadam:

  • E extends ElementType = "span" — tur parametri, standart — "span". as berilmasa, E = "span".
  • <Text as="a" …> — TypeScript as dan E = "a" ni chiqardi. Props turi endi ComponentProps<"a"> ni o'z ichiga oladi — href ruxsat.
  • const Tag = as ?? "span" — JSX'da katta harfli o'zgaruvchi komponent sifatida ishlatiladi (JSX aslida JavaScript).

Noto'g'ri kombinatsiya:

tsx
import type { ComponentProps, ElementType, ReactNode } from "react";

type TextProps<E extends ElementType> = {
  as?: E;
  children: ReactNode;
} & Omit<ComponentProps<E>, "as" | "children">;

function Text<E extends ElementType = "span">({
  as,
  ...rest
}: TextProps<E>) {
  const Tag = as ?? "span";
  return <Tag {...rest} />;
}

export default function App() {
  return (
    <Text as="span" href="https://example.com/menyu">
      Menyu
    </Text>
  );
}

Xato:

text
TS2322: Type '{ as: "span"; href: string; children: string; }' is not assignable to type 'IntrinsicAttributes & { as?: "span" | undefined; children: ReactNode; } & Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "as" | "children">'.
  Property 'href' does not exist on type 'IntrinsicAttributes & { as?: "span" | undefined; children: ReactNode; } & Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "as" | "children">'. Did you mean 'ref'?

Xabar uzun, lekin oxirgi qismi aniq: "href <span> ning props'ida yo'q. ref ni nazarda tutdingizmi?". <span> da href — ma'nosiz, havola bo'lmaydi va klaviatura bilan ochilmaydi.

4.3 Narxi

Polymorphic turlar kuchli, lekin qimmat:

  • Xato xabarlari uzun — yuqoridagidek. Jamoadoshingiz uni o'qishga qiynaladi.
  • tsc sekinlashadi. Har <Text as=…> uchun ComponentProps<E> qayta hisoblanadi. Katta loyihada (yuzlab ishlatilish) bu soniyalarda sezilarli bo'lishi mumkin.
  • ref turi murakkablashadi: as="a" da HTMLAnchorElement, as="button" da boshqa.

Shuning uchun ko'p dizayn tizimlari as o'rniga asChild naqshini tanladi (Radix, shadcn/ui): komponent o'z tegini emas, bolaning tegini ishlatadi (Polymorphic komponentlar). vazifalar da polymorphic komponent yo'q — kerak bo'lmadi. Qoida: as ni faqat haqiqatan bir necha xil teg kerak bo'lganda qo'llang (tipografiya, Box, Stack).

5. satisfies: tekshir, lekin turini o'zgartirma

Komponent atrofida ko'pincha konfiguratsiya obyektlari bo'ladi: variant → rang, holat → matn. Ularni turga solishtirish kerak — hamma kalit bormi? Lekin tur annotatsiyasi (: Record<…>) obyektning aniq turini "kengaytirib" yuboradi. satisfies ikkalasini beradi:

tsx
type Variant = "oddiy" | "asosiy" | "xavfli";

const colors = {
  oddiy: "#e8efe9",
  asosiy: "#1f7a4d",
} satisfies Record<Variant, string>;

export default function App() {
  return <p style={{ color: colors.asosiy }}>Saqlash</p>;
}

Xato:

text
TS2741: Property 'xavfli' is missing in type '{ oddiy: string; asosiy: string; }' but required in type 'Record<Variant, string>'.

"xavfli yetishmayapti." Yangi variant qo'shilsa — rang yozish unutilmaydi. Nega const colors: Record<Variant, string> = … emas? Annotatsiya bilan colors turi — Record<Variant, string>, ya'ni "har kalit — qandaydir satr". satisfies bilan esa tur — obyektning o'zi: { oddiy: string; asosiy: string; xavfli: string }. Qiymatlar literal bo'lishi kerak bo'lsa, as const satisfies … qo'shiladi. Farq kichik ko'rinadi, lekin konfiguratsiyadan tur chiqarishda (keyof typeof colors) muhim.

Yozuv Tekshiradimi Obyekt turi
const c = {…} yo'q aniq
const c: Record<V, string> = {…} ha kengaygan
const c = {…} satisfies Record<V, string> ha aniq

6. Vazifalar qadami: Tugma — nomi bo'lishi shart

6.1 Tur o'zgarishi

refactor/turlar dagi to'rtinchi bo'lak — birinchi haqiqiy kod o'zgarishi. ui/Tugma.tsx da props turi (kanondan aynan):

tsx
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";

type TugmaVarianti = "oddiy" | "asosiy" | "xavfli";

// 18/#37: discriminated union — tugmaning NOMI bo'lishi shart (ekran
// o'quvchi uchun): yo matn (children), yo aria-label. Matnsiz va
// aria-label'siz <Tugma /> — tsc xatosi (o'lchandi, natijalar/38-*)
type Nomi =
  | { children: ReactNode; "aria-label"?: string }
  | { children?: never; "aria-label": string };

type Props = Omit<
  ComponentPropsWithoutRef<"button">,
  "children" | "aria-label"
> &
  Nomi & {
    variant?: TugmaVarianti;
    ref?: Ref<HTMLButtonElement> | undefined;
  };

Uch darsning bilimi bir joyda: ComponentPropsWithoutRef (36-dars), Omit — native children va aria-label olib tashlanadi, chunki ular endi Nomi da boshqacha turlangan. Nomi — bugungi never bilan istisno. Komponentning tanasi o'zgarmadi — o'zgarish faqat turda.

6.2 O'lchov

Tur ishlashini kanon vaqtinchalik fayl bilan tekshirdi — uch xil tugma (natijalar/38-turlar.txt):

tsx
import { Tugma } from "./ui/Tugma.tsx";
export const nomsiz = <Tugma onClick={() => {}} />;
export const belgili = <Tugma aria-label="Yopish" onClick={() => {}} />;
export const matnli = <Tugma>Saqlash</Tugma>;

Haqiqiy natija — faqat bitta xato, nomsiz da:

text
src/sinov-tugma.tsx(2,24): error TS2322: Type '{ onClick: () => void; }' is not assignable to type 'IntrinsicAttributes & Props'.
  Property '"aria-label"' is missing in type '{ onClick: () => void; }' but required in type '{ children?: never; "aria-label": string; }'.

"{ onClick } turida aria-label yo'q, lekin { children?: never; "aria-label": string } da u majburiy." tsc ikkinchi variantni tanladi (children yo'q) va unga nima yetishmasligini aytdi. Xabardagi children?: never — exactOptionalPropertyTypes yoqilgani uchun aynan shunday ko'rinadi (bayroqsiz tsc uni children?: undefined deb yozardi).

Shu xatoni o'zingiz ham jonli ko'ring — kanondagi turning qisqa nusxasi:

tsx
// @ts-options: {"exactOptionalPropertyTypes": true}
import type { ComponentProps, ReactNode } from "react";

type Name =
  | { children: ReactNode; "aria-label"?: string }
  | { children?: never; "aria-label": string };

type ButtonProps = Omit<
  ComponentProps<"button">,
  "children" | "aria-label"
> &
  Name;

function Button(props: ButtonProps) {
  return <button type="button" {...props} />;
}

export default function App() {
  return <Button onClick={() => {}} />;
}

Xato:

text
TS2322: Type '{ onClick: () => void; }' is not assignable to type 'IntrinsicAttributes & ButtonProps'.
  Property '"aria-label"' is missing in type '{ onClick: () => void; }' but required in type '{ children?: never; "aria-label": string; }'.

Kanondagi 9 ta Tugma ishlatilishi o'zgarishsiz o'tdi — hammasida matn bor edi. Demak qoida bugun hech narsani buzmadi, ertangi nomsiz tugmani esa to'xtatadi. 39-darsdagi axe tekshiruvi buni ish vaqtida ham tasdiqlaydi (React'da accessibility asoslari).

Tekshiruvlar: npm run tip — 0 xato (vaqtinchalik fayl olib tashlangach), npm run lint, npm test — toza. Commit hali yo'q — keyingi darsda.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
TS17008: JSX element 'T' has no corresponding closing tag .tsx da <T>(…) => <T,> yoki function
renderItem da 'd' is of type 'unknown' generic o'rniga unknown List<T>, (item: T) => …
TS2339 … does not exist on type '{…} | {…}' union props ...rest bilan destrukturizatsiya avval props.kind ni tekshiring
ikki variant aralashib o'tdi ?: never yo'q istisno variantga x?: never
uzun Omit<DetailedHTMLProps<…>> xabari polymorphic as oxirgi qatorni o'qing; asChild ni ko'ring
TS2741 … is missing … Record<…> satisfies — kalit yetishmaydi kalitni qo'shing

8. Mashqlar

Mashqlar kurs/mashqlar/18/37-generic-union/ da.

1-mashq (oson): o'tadimi?

type Name = { children: ReactNode; "aria-label"?: string } | { children?: never; "aria-label": string }. ha yoki yo'q:

  • <Button>Saqlash</Button>
  • <Button aria-label="Yopish" />
  • <Button />
  • <Button aria-label="Savatni yopish">✕ Yopish</Button>
Yechim
  • ha — 1-variant: children bor.
  • ha — 2-variant: children yo'q, aria-label bor.
  • yo'q — hech bir variant: 1-variantda children majburiy, 2-variantda aria-label.
  • ha — 1-variant: children bor, aria-label ixtiyoriy va ruxsat. 2-variantga to'g'ri kelmaydi (children — never), lekin bitta variant yetarli.

2-mashq (o'rta): generic Select

Generic Select<T> yozing: options: readonly T[], getLabel: (o: T) => string, onPick: (o: T) => void, label: string. Ichida <label> + <select>, qiymat — indeks (String(i)). Tanlanganda onPick(options[i]) chaqirilsin. App — taomlar ({ name, price }), tanlansa konsolga tanlandi: Manti 30000. Ishora: Number(e.target.value) — indeks; options[i] turi T | undefined bo'lishi mumkin (noUncheckedIndexedAccess bo'lsa) — tekshiring.

Yechim
tsx
type SelectProps<T> = {
  label: string;
  options: readonly T[];
  getLabel: (option: T) => string;
  onPick: (option: T) => void;
};

function Select<T>(props: SelectProps<T>) {
  const { label, options, getLabel, onPick } = props;
  return (
    <p>
      <label htmlFor="tanlov">{label}</label>{" "}
      <select
        id="tanlov"
        onChange={(e) => {
          const option = options[Number(e.target.value)];
          if (option !== undefined) onPick(option);
        }}
      >
        {options.map((o, i) => (
          <option key={i} value={i}>
            {getLabel(o)}
          </option>
        ))}
      </select>
    </p>
  );
}

const dishes = [
  { name: "Osh", price: 35000 },
  { name: "Manti", price: 30000 },
];

export default function App() {
  return (
    <Select
      label="Taom"
      options={dishes}
      getLabel={(d) => d.name}
      onPick={(d) => console.log("tanlandi:", d.name, d.price)}
    />
  );
}

Konsolda:

text
tanlandi: Manti 30000

Ekranda:

text
Taom OshManti

onPick={(d) => …} dagi d — { name: string; price: number }, options dan chiqarildi. T ni hech qayerda yozmadik. Ekrandagi OshManti — <option> lar matni, tekshiruvchi ularni bitta qatorda ko'rsatadi. key={i} — bu yerda maqbul: ro'yxat o'zgarmaydi (key).

3-mashq (qiyin): Notice — uch xil xabar

Notice komponenti uch turdan birini oladi:

  • { kind: "info"; text: string } — oddiy <p>.
  • { kind: "error"; text: string; retry: () => void } — role="alert" li <p> va «Qayta urinish» tugmasi.
  • { kind: "loading" } — Yuklanmoqda…, text bo'lmasin.

Komponent ichida switch (props.kind) va never bilan to'liqlik tekshiruvi. App da error turini «Qayta urinish» bilan ko'rsating, bosilsa konsolga qayta…. Ishora: «Tuzoq: destrukturizatsiya union'ni buzadi» bo'limi — props ni butun holida tekshiring; never — React + TS II.

Yechim
tsx
type NoticeProps =
  | { kind: "info"; text: string }
  | { kind: "error"; text: string; retry: () => void }
  | { kind: "loading" };

function Notice(props: NoticeProps) {
  switch (props.kind) {
    case "info":
      return <p>{props.text}</p>;
    case "error":
      return (
        <p role="alert">
          {props.text}{" "}
          <button onClick={props.retry}>Qayta urinish</button>
        </p>
      );
    case "loading":
      return <p>Yuklanmoqda…</p>;
    default: {
      const unhandled: never = props;
      return unhandled;
    }
  }
}

export default function App() {
  return (
    <Notice
      kind="error"
      text="Menyu yuklanmadi"
      retry={() => console.log("qayta…")}
    />
  );
}

Konsolda:

text
qayta…

Ekranda:

text
Menyu yuklanmadi Qayta urinish

case "error" ichida props.retry — xatosiz, boshqa tarmoqlarda esa yo'q. <Notice kind="loading" text="…" /> — tsc xatosi, <Notice kind="error" text="…" /> (retry siz) — ham. Yangi kind qo'shilsa, never qatori case yozishni eslatadi.

4-mashq: Vazifalar qadami

Tugma.tsx ga Nomi union'ini qo'shing (Vazifalar qadami bo'limidagi parcha). Keyin o'lchang: src/sinov-tugma.tsx vaqtinchalik faylida uch tugma, npm run tip — xato faqat nomsizda. Faylni o'chiring.

Yechim

Tekshiruv ro'yxati:

  • npm run tip vaqtinchalik fayl bilan — bitta TS2322 (Property '"aria-label"' is missing …), belgili va matnli — xatosiz.
  • Fayl o'chirilgach — 0 xato. Mavjud 9 ta <Tugma> o'zgarishsiz o'tdi.
  • npm run lint, npm test — toza. Commit — keyingi darsda.

9. Real ishda

  • Generic komponentlar — jadval (TanStack Table useReactTable<Row>), ro'yxat, Select, Combobox — dizayn tizimlarining asosi. Ular T ni data prop'idan chiqaradi.
  • Union props — Button (href bo'lsa <a>, bo'lmasa <button>), Alert (turiga qarab tugmalar), forma maydonlari (type="number" bo'lsa min/max).
  • never bilan istisno — "yo value+onChange, yo defaultValue" (Controlled va uncontrolled API) qoidasini ham shunday yozish mumkin.
  • Polymorphic — Chakra/MUI'da as/component, Radix'da asChild. Intervyuda "polymorphic komponentni qanday turlaysiz?" — senior darajadagi savol.
  • satisfies — konfiguratsiya, marshrutlar ro'yxati, tarjima kalitlari (keyingi darsda).

Xulosa

  • Generic komponent — function List<T>(props: ListProps<T>); T props'dan chiqariladi, kerak bo'lsa <List<Dish> …>. .tsx da strelka — <T,>.
  • Discriminated union props — "yo bu, yo u": kind bo'yicha tsc to'g'ri variantni tanlaydi. ...rest bilan destrukturizatsiya — bog'lanishni uzadi.
  • Ajratuvchisiz istisno — ?: never: { children } | { children?: never; "aria-label" }.
  • Polymorphic as — E extends ElementType + ComponentProps<E>; kuchli, lekin xabarlar uzun va tsc sekinroq.
  • satisfies — tekshiradi, aniq turni saqlaydi.
  • vazifalar: Tugma — nomi bo'lishi shart; nomsiz tugma — TS2322, 9 ta mavjud ishlatilish o'zgarishsiz o'tdi.

Keyingi dars: Kutubxona turlari va module augmentation — @types paketlari, declare module va kutubxonani "register" qilish.

Manbalar

  • React: "Using TypeScript" — react.dev/learn/typescript (2026-10 holatiga, React 19.3)
  • TypeScript: Generics, Narrowing (discriminated unions), satisfies — typescriptlang.org/docs/handbook
  • TypeScript 4.6: "Control-flow analysis for destructured discriminated unions" — typescriptlang.org/docs/handbook/release-notes/typescript-4-6.html
  • Radix UI: Composition — asChild — radix-ui.com/primitives/docs/guides/composition
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React + TS IV: generic, polymorphic va discriminated union props — xatoni kompilyatsiyada ushlash — IlmHamroh