Բոլոր դասերը
ԼեզուներՍկսնակ12 րոպե ընթերցումԻմ ստեկում

JavaScript և TypeScript

JavaScript-ի այն մասը, որն իսկապես կարևոր է, և տիպերը, որոնք կանգնեցնում են բագերը մինչև production հասնելը։

  • TypeScript 5
  • Types & generics
  • Strict mode
  • ES2024
Քայլ 1/5
Ֆունկցիա՝ տիպերով․ price-ը պետք է number լինի։ Type checker-ը (tsc) հերթապահում է՝ խոշորացույցը ձեռքին։

30 վայրկյանում

JavaScript-ը բրաուզերի և Node.js-ի լեզուն է։ TypeScript-ը JavaScript է՝ վրան տիպերի համակարգով. դու նկարագրում ես տվյալների կառուցվածքը, իսկ կոմպիլյատորը ստուգում է յուրաքանչյուր օգտագործում դեռ կոդը գործարկելուց առաջ։ Տիպերը build-ի ժամանակ անհետանում են, այնպես որ արդյունքը սովորական JavaScript է, պարզապես production-ում շատ ավելի քիչ անակնկալներով։

Ինչու եմ օգտագործում

  • TypeScript-ն օգտագործում եմ գրեթե ամեն ինչում, որ կառուցում եմ՝ Next.js խանութներից մինչև NestJS API և React Native հավելվածներ, որովհետև դաշտի մեկ սխալ անունը պետք է ձախողվի էդիտորում, ոչ թե հաճախորդի աչքի առաջ։
  • Տիպերին վերաբերվում եմ որպես կենդանի փաստաթղթի. երբ backend-ի պատասխանը փոխվում է, կոմպիլյատորը ցույց է տալիս բոլոր էկրաններն ու սերվիսները, որոնք պետք է հետևեն այդ փոփոխությանը։
  • Strict ռեժիմը միացնում եմ առաջին օրվանից։ Մեծ կոդային բազայում այն հետո ներդնելը շատ ավելի թանկ է, քան դրանով սկսելը։
  • JavaScript-ի հիմունքները (closure-ներ, promise-ներ, event loop) դեռ համարում եմ իրական հիմքը։ TypeScript-ը չի փոխում, թե ինչպես է կոդն աշխատում, միայն թե որքան ապահով ես այն գրում։

Ինչպես է աշխատում

01JavaScript-ի հիմունքները, որոնք իսկապես կարևոր են

Տիպերից առաջ հասկացիր runtime-ը։ JavaScript-ն ունի պրիմիտիվ արժեքներ (string, number, boolean, null, undefined, bigint, symbol) և օբյեկտներ, որոնք փոխանցվում են հղումով. երկու փոփոխական կարող են ցույց տալ նույն օբյեկտը, ուստի մեկը փոփոխելիս փոխվում է նաև այն, ինչ տեսնում է մյուսը։ Լռելյայն օգտագործիր const, իսկ let՝ միայն երբ պետք է վերագրել. var-ից խուսափիր։ Closure-ը ֆունկցիա է, որը հիշում է այն վայրի փոփոխականները, որտեղ ստեղծվել է, նույնիսկ արտաքին ֆունկցիայի ավարտից հետո։ Closure-ների վրա են հիմնված callback-ները, React hook-երը, debounce-ը և մասնավոր վիճակը։ Կարևոր է նաև հավասարությունը. միշտ օգտագործիր === (խիստ հավասարություն), որովհետև ==-ը կատարում է շփոթեցնող տիպային փոխակերպումներ։ Վերջապես, սովորիր ժամանակակից սինտաքսը, որը կկարդաս ամեն օր՝ destructuring, spread, optional chaining (a?.b) և nullish coalescing (a ?? fallback), որը fallback է անում միայն null-ի կամ undefined-ի դեպքում, ի տարբերություն ||-ի, որը փոխարինում է նաև 0-ն ու դատարկ տողերը։

ts
function makeCounter() {
  let count = 0; // private, captured by the closure
  return () => ++count;
}
 
const next = makeCounter();
next(); // 1
next(); // 2
 
const user = { name: "Ana", address: null };
const city = user.address?.city ?? "Unknown"; // "Unknown"
const items = 0 || 10; // 10  (|| replaces 0)
const safe = 0 ?? 10; // 0   (?? keeps 0)

02Ասինխրոն կոդ և event loop

JavaScript-ը քո կոդն աշխատեցնում է մեկ thread-ի վրա։ Դանդաղ աշխատանքը՝ ցանցային հարցումներ, թայմերներ, ֆայլերի ընթերցում, փոխանցվում է բրաուզերին կամ Node.js-ին, և ավարտվելիս callback է դրվում հերթում։ Event loop-ը հերթից աշխատանք է վերցնում միայն այն ժամանակ, երբ call stack-ը դատարկ է։ Կա երկու հիմնական հերթ՝ microtask-ներ (promise callback-ներ, await-ից հետո շարունակություններ) և macrotask-ներ (setTimeout, I/O, UI իրադարձություններ)։ Յուրաքանչյուր task-ից հետո բոլոր microtask-ները կատարվում են մինչև հաջորդ macrotask-ը։ Ահա թե ինչու resolved promise-ը log է անում 0 ms-անոց setTimeout-ից առաջ։ async/await-ը promise-ների վրա սինտաքս է. await-ը կանգնեցնում է տվյալ ֆունկցիան, ոչ թե ամբողջ ծրագիրը։ Սրանից բխում է երկու գործնական կանոն։ Անկախ հարցումներն աշխատեցրու զուգահեռ Promise.all-ով՝ մեկ առ մեկ await անելու փոխարեն, և երբեք մի արգելափակիր thread-ը ծանր սինխրոն ցիկլերով, որովհետև մինչև դրանց ավարտը ոչ մի այլ բան (click-եր, այլ հարցումներ) չի կարող աշխատել։

ts
console.log("1 sync");
setTimeout(() => console.log("4 macrotask"), 0);
Promise.resolve().then(() => console.log("3 microtask"));
console.log("2 sync");
 
// Parallel, not sequential:
async function loadDashboard(id: string) {
  const [user, orders] = await Promise.all([
    fetch(`/api/users/${id}`).then((r) => r.json()),
    fetch(`/api/orders?user=${id}`).then((r) => r.json()),
  ]);
  return { user, orders };
}

03Տիպեր՝ տվյալների կառուցվածքի նկարագրություն

Տիպը խոստում է արժեքի մասին. սա string է, այս օբյեկտն ունի id և email, այս ֆունկցիան վերադարձնում է number։ TypeScript-ը այդ խոստումները ստուգում է կոմպիլյացիայի ժամանակ։ Օբյեկտների կառուցվածքի համար հիմնականում կօգտագործես type alias-ներ և interface-ներ, «սրանցից մեկը» իմաստի համար՝ union տիպեր ("draft" | "paid" | "cancelled"), իսկ ճշգրիտ արժեքների համար՝ literal տիպեր։ Լոկալ փոփոխականների համար թող inference-ն աշխատի, իսկ սահմաններում տիպերը գրիր բացահայտ՝ ֆունկցիաների պարամետրեր, public ֆունկցիաների վերադարձվող տիպեր, API պատասխաններ և կոմպոնենտների props։ Խուսափիր any-ից, որն անջատում է ստուգումը. դեռ չվալիդացված արժեքների համար օգտագործիր unknown, որովհետև unknown-ը ստիպում է ստուգել նախքան օգտագործելը։ satisfies օպերատորը օգտակար է config օբյեկտների համար. այն ստուգում է արժեքը տիպի դեմ՝ պահպանելով դրա ճշգրիտ inferred literal տիպերը։

ts
type OrderStatus = "draft" | "paid" | "cancelled";
 
interface Order {
  id: string;
  total: number;
  status: OrderStatus;
  note?: string; // optional
}
 
function label(order: Order): string {
  return `#${order.id} - ${order.status}`;
}
 
const routes = {
  home: "/",
  orders: "/orders",
} satisfies Record<string, `/${string}`>;

04Narrowing և discriminated union-ներ

Narrowing-ը այն է, թե ինչպես է TypeScript-ը հետևում քո ստուգումներին։ if (typeof x === "string")-ի ներսում կոմպիլյատորը գիտի, որ x-ը string է։ Այն հասկանում է typeof, instanceof, in օպերատորը, հավասարության ստուգումները, truthiness-ը և քո սեփական type predicate-ները (ֆունկցիաներ, որոնք վերադարձնում են value is Type)։ Ամենահզոր pattern-ը discriminated union-ն է. մի քանի օբյեկտային տիպեր ունեն մեկ ընդհանուր literal դաշտ, օրինակ kind կամ status, և այդ դաշտով switch-ը նեղացնում է ճիշտ մեկ տարբերակի։ default ճյուղում ավելացրու never ստուգում, և կոմպիլյատորը կձախողի build-ը այն օրը, երբ ինչ-որ մեկը նոր տարբերակ ավելացնի ու մոռանա այն մշակել։ Սա «մի դեպք մոռացանք» տիպի բագերը, որոնք շատ տարածված են վճարումների և պատվերների հոսքերում, դարձնում է կոմպիլյացիայի սխալներ։

ts
type PaymentResult =
  | { kind: "success"; transactionId: string }
  | { kind: "declined"; reason: string }
  | { kind: "unknown" }; // gateway timed out
 
function message(result: PaymentResult): string {
  switch (result.kind) {
    case "success":
      return `Paid: ${result.transactionId}`;
    case "declined":
      return `Declined: ${result.reason}`;
    case "unknown":
      return "Checking with the bank...";
    default: {
      const unreachable: never = result; // new variant = compile error
      return unreachable;
    }
  }
}

05Generic-ներ և utility տիպեր

Generic-ը տիպի պարամետր է՝ տեղապահ, որը լրացվում է ֆունկցիան կամ տիպն օգտագործելիս։ Այն թույլ է տալիս գրել մեկ կոդ, որն աշխատում է բազմաթիվ տիպերի հետ՝ առանց ինֆորմացիա կորցնելու։ JSON բերող ֆունկցիան կարող է վերադարձնել Promise<T>, և կանչողը ստանում է User կամ Order, ոչ թե any։ Սահմանափակումները (T extends { id: string }) ասում են, թե տիպը նվազագույնը ինչ պետք է ունենա։ TypeScript-ը generic-ների վրա կառուցված utility տիպեր ունի. Partial<T>-ն բոլոր դաշտերը դարձնում է ոչ պարտադիր (հարմար է update payload-ների համար), Pick-ն ու Omit-ը ընտրում են դաշտեր, Record<K, V>-ն նկարագրում է բառարաններ, ReturnType-ն ու Awaited-ը տիպեր են հանում ֆունկցիաներից։ Հիշիր, որ generic cast-ը վալիդացիա չէ. ցանցից եկած տվյալները պետք է runtime-ում ստուգվեն schema գրադարանով, օրինակ Zod-ով կամ Valibot-ով, նախքան դրանց տիպին վստահելը։

ts
async function getJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as T; // validate with a schema in real code
}
 
function byId<T extends { id: string }>(items: T[]): Record<string, T> {
  return Object.fromEntries(items.map((item) => [item.id, item]));
}
 
type User = { id: string; email: string; name: string };
type UserUpdate = Partial<Omit<User, "id">>;
 
const user = await getJson<User>("https://api.example.com/me");

06Strict ռեժիմ և tsconfig-ի հիմնականը

tsconfig.json-ը որոշում է, թե որքան խիստ է կոմպիլյատորը և ինչպես է այն հասկանում մոդուլները։ Ամենակարևոր տարբերակը strict: true-ն է, որը միացնում է strictNullChecks (null-ը և undefined-ը պետք է մշակվեն), noImplicitAny և մի քանի այլ ստուգումներ։ TypeScript 6.0-ից սկսած strict-ը նոր config-ների համար լռելյայն է, և TypeScript 7.0-ն (Go-ով գրված նոր կոմպիլյատորը, թողարկված 2026-ին և մեծ build-երի վրա մոտ տասն անգամ ավելի արագ) պահպանում է այդ լռելյայն արժեքը։ Ավելացրու noUncheckedIndexedAccess, որպեսզի array[i]-ն ու բառարանային որոնումները ներառեն undefined։ Bundler-ով կառուցվող հավելվածների համար (Next.js, Vite) օգտագործիր module "esnext" և moduleResolution "bundler", իսկ Node.js փաթեթների համար՝ "nodenext"։ Միացրու verbatimModuleSyntax, որպեսզի միայն տիպերի import-ները բացահայտ լինեն, և դիր noEmit, երբ իրական կոմպիլյացիան անում է bundler-ը։ CI-ում աշխատեցրու tsc --noEmit, որպեսզի տիպի սխալն արգելափակի merge-ը։

json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2024", "DOM", "DOM.Iterable"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noEmit": true,
    "skipLibCheck": true,
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["src"]
}

Օգտագործեք, երբ

  • Ցանկացած նախագիծ, որը կապրի մի քանի շաբաթից ավելի կամ որի վրա կաշխատի մեկից ավելի մարդ։
  • Սահմաններ հատող կոդ՝ API client-ներ, ընդհանուր փաթեթներ, monorepo-ներ, որտեղ backend-ը և մի քանի frontend-ներ կիսում են տիպերը։
  • Բազմաթիվ վիճակներով և կանոններով ոլորտներ, ինչպես պատվերները, վճարումներն ու բաժանորդագրությունները, որտեղ discriminated union-ները բռնում են բաց թողնված դեպքերը։
  • Մեծ refactor-ներ. կոմպիլյատորը տալիս է այն ամենի ամբողջական ցուցակը, ինչ փոփոխությունը կոտրում է։

Մի՛ օգտագործեք, երբ

  • Տասը տողանոց մեկանգամյա script կամ արագ prototype, որը վաղը կջնջես. մաքուր JavaScript-ը (ցանկության դեպքում JSDoc տիպերով) բավական է։
  • Որպես runtime վալիդացիայի փոխարինում. տիպերը build-ի ժամանակ անհետանում են, ուստի օգտատիրոջ մուտքագրումն ու API պատասխանները դեռ պետք է ստուգել։

Այլընտրանքներ

  • Plain JavaScript + JSDoc

    Տիպային մեկնաբանություններ, որոնք ստուգում է TypeScript-ի շարժիչը՝ առանց build քայլի. լավ է փոքր գրադարանների և script-ների համար։

  • Flow

    Meta-ի type checker-ը JavaScript-ի համար. դեռ օգտագործվում է React Native-ի որոշ ներքին մասերում, բայց էկոհամակարգը հիմնականում անցել է TypeScript-ի։

  • Kotlin/JS, Dart, ReScript

    Լեզուներ, որոնք կոմպիլյացվում են JavaScript-ի՝ ավելի ուժեղ երաշխիքներով, բայց ավելի փոքր էկոհամակարգի և վեբ թիմերի մեծ մասի համար անծանոթ սինտաքսի գնով։

Լավագույն պրակտիկաներ

  1. Նախագծի սկզբում միացրու strict և noUncheckedIndexedAccess. CI-ում աշխատեցրու tsc --noEmit։
  2. Սահմանները տիպավորիր բացահայտ (props, ֆունկցիաների signature-ներ, API պատասխաններ), իսկ լոկալ փոփոխականները թող inference-ին։
  3. any-ի փոխարեն նախընտրիր unknown, և արտաքին տվյալները վալիդացրու schema գրադարանով՝ նախքան դրանց տիպին վստահելը։
  4. Վիճակները մոդելավորիր discriminated union-ներով և ավելացրու never ստուգում, որպեսզի նոր դեպքերը հնարավոր չլինի մոռանալ։
  5. Ընդհանուր տիպերի համար պահիր ճշմարտության մեկ աղբյուր, օրինակ ընդհանուր փաթեթ կամ OpenAPI-ից կամ տվյալների բազայի schema-ից գեներացված տիպեր։
  6. Անկախ ասինխրոն աշխատանքի համար օգտագործիր Promise.all և միշտ մշակիր rejected promise-ները։

Տարածված սխալներ

  • Սխալները լռեցնելու համար ամենուր any և as cast-եր ցրելը, որը թաքցնում է հենց այն բագերը, որոնք TypeScript-ը պետք է բռներ։
  • Տիպավորված API պատասխանին վստահելը առանց runtime վալիդացիայի, հետո crash, երբ backend-ը null է ուղարկում։
  • Անկախ հարցումները մեկը մյուսի հետևից await անելը, ինչը էջը մի քանի անգամ ավելի դանդաղ է դարձնում, քան պետք է։
  • Լռելյայն արժեքների համար || օգտագործելը և պատահաբար 0-ի կամ դատարկ տողի պես վավեր արժեքներ փոխարինելը։
  • Մոռանալը, որ օբյեկտները կիսվում են հղումով, և փոփոխել վիճակ, որը այլ կոդ դեռ կարդում է։

Խորը վերլուծություն

07Կառուցվածքային տիպավորում, ավելորդ property-ների ստուգում և branded տիպեր

TypeScript-ը տիպերը համեմատում է ըստ կառուցվածքի, ոչ թե ըստ անվան։ Եթե օբյեկտն ունի տիպի պահանջած բոլոր property-ները, այն վերագրելի է, նույնիսկ եթե հայտարարվել է որպես այլ բան։ Սա կոչվում է structural typing, և հենց դրա պատճառով UserId-ն ու OrderId-ն, որոնք երկուսն էլ սովորական string են, կարելի է իրար տեղ փոխանցել՝ առանց որևէ սխալի։ Ավելորդ property-ների ստուգումը նեղ բացառություն է. օբյեկտային literal-ը, որը գրված է անմիջապես այնտեղ, որտեղ տիպ է սպասվում, չի կարող անհայտ key-եր պարունակել, բայց նույն օբյեկտը, փոխանցված փոփոխականի միջոցով, ընդունվում է։ Երբ երկու արժեք ունեն նույն կառուցվածքը, բայց երբեք չպետք է խառնվեն, օգտագործիր branded տիպ՝ primitive-ը հատիր եզակի marker property-ով, որը գոյություն ունի միայն տիպերի մակարդակում։ Փոքր constructor ֆունկցիան դառնում է brand ստեղծելու միակ ճանապարհը, այնպես որ վալիդացիան և տիպավորումը մի տեղում են։ Brand-երը runtime-ում ոչինչ չարժեն, որովհետև marker-ը ջնջվում է մնացած տիպերի հետ միասին։

ts
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };
 
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
 
function toUserId(raw: string): UserId {
  if (!/^usr_[a-z0-9]+$/.test(raw)) throw new Error("Invalid user id");
  return raw as UserId; // the only cast, guarded by validation
}
 
declare function cancelOrder(id: OrderId): Promise<void>;
 
const id = toUserId("usr_42");
// cancelOrder(id); // error: UserId is not assignable to OrderId

08Տիպերի մակարդակում ծրագրավորում՝ conditional, mapped և template literal տիպեր

Partial-ի և ReturnType-ի նման utility տիպերը մոգություն չեն. դրանք կառուցված են երեք հնարավորությունից, որոնք կարող ես ինքդ օգտագործել։ Conditional տիպը (T extends U ? X : Y) ընտրում է տիպ՝ կախված մեկ այլ տիպից, իսկ infer-ը վերցնում է համընկած տիպի մի մասը. հենց այդպես է ReturnType-ը հանում ֆունկցիայի արդյունքը։ Երբ T-ն union է, conditional տիպը բաշխվում է յուրաքանչյուր անդամի վրա առանձին. դա կանխելու համար երկու կողմերը փաթաթիր քառակուսի փակագծերում ([T] extends [U])։ Mapped տիպը ({ [K in keyof T]: ... }) ձևափոխում է ամեն property, իսկ as-ով key remapping-ը կարող է key-երը վերանվանել կամ զտել։ Template literal տիպերը կառուցում են string տիպեր, օրինակ՝ `on${Capitalize<K>}` event handler-ների անունների համար։ Սրանք օգտագործիր գրադարանային և ենթակառուցվածքային կոդում՝ տիպավորված API client-ներ, ձևերի helper-ներ, event emitter-ներ, որտեղ մեկ խելացի տիպը փոխարինում է հարյուրավոր ձեռքով գրվածների։ Հավելվածի կոդում դրանք քիչ օգտագործիր. տիպը, որը բացատրելու համար գրատախտակ է պետք, սպասարկման ծախս է։

ts
// Pick only the keys whose values are functions
type MethodKeys<T> = {
  [K in keyof T]: T[K] extends (...args: never[]) => unknown ? K : never;
}[keyof T];
 
// Turn { click: MouseEvent } into { onClick: (e: MouseEvent) => void }
type Handlers<E> = {
  [K in keyof E & string as `on${Capitalize<K>}`]: (event: E[K]) => void;
};
 
type Unwrap<T> = T extends Promise<infer V> ? V : T;
 
type A = Handlers<{ click: MouseEvent; keydown: KeyboardEvent }>;
// { onClick: (event: MouseEvent) => void; onKeydown: (event: KeyboardEvent) => void }
type B = Unwrap<Promise<number>>; // number

09Soundness-ի բացեր, որոնց մասին պետք է իմանաս

TypeScript-ը դիտավորյալ ամբողջովին sound չէ. որոշ ծրագրեր անցնում են տիպերի ստուգումը և այնուամենայնիվ ձախողվում են runtime-ում։ Բացերն իմանալը ցույց է տալիս, թե որտեղ պետք է զգոն մնալ։ Զանգվածները covariant են, այնպես որ Dog[]-ը կարելի է փոխանցել այնտեղ, որտեղ Animal[] է սպասվում, և կանչվող ֆունկցիան կարող է դրա մեջ Cat ավելացնել։ Method syntax-ով հայտարարված պարամետրերը (save(x: T): void) ստուգվում են bivariant՝ նույնիսկ strictFunctionTypes-ի դեպքում. property syntax-ը (save: (x: T) => void) ստուգվում է խիստ, ուստի այն նախընտրիր քո վերահսկած interface-ներում։ Type assertion-ները (as), non-null օպերատորը (!) և any-ն շրջանցում են ստուգումները։ Ինդեքսով մուտքը՝ առանց noUncheckedIndexedAccess-ի, ձևացնում է, թե ամեն key գոյություն ունի։ Օբյեկտի property-ի նեղացումը չի չեղարկվում ֆունկցիայի կանչերով, այնպես որ await-ից կամ callback-ից առաջ ստուգված property-ն կարող է փոխված լինել, երբ այն օգտագործես։ Վերջապես, enum-ները և namespace-ները runtime կոդ են գեներացնում՝ ի տարբերություն գրեթե մնացած ամեն ինչի. erasableSyntaxOnly տարբերակը (TypeScript 5.8) արգելում է դրանք, որպեսզի կոդը կարողանա աշխատել պարզ type stripping-ով, ինչպես հիմա լռելյայն անում է Node.js-ը։

ts
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
 
function addCat(list: Animal[]) {
  list.push({ name: "Cat" }); // allowed: arrays are covariant
}
 
const dogs: Dog[] = [{ name: "Rex", bark() {} }];
addCat(dogs);
dogs[1].bark(); // type-checks, crashes at runtime
 
// Safer: accept a readonly view when you only read
function names(list: readonly Animal[]) {
  return list.map((a) => a.name);
}

10Կոմպիլյատորի արագությունը մեծ կոդային բազաներում

Մեծ monorepo-ում տիպերի ստուգման ժամանակը դառնում է developer experience-ի խնդիր։ Սկսիր չափումից. tsc --extendedDiagnostics-ը տպում է ստուգման ժամանակը, հիշողությունը և ստեղծված տիպերի քանակը, իսկ --generateTrace-ը գրում է trace, որը կարող ես բացել բրաուզերի performance գործիքում և գտնել այն ֆայլերն ու տիպերը, որոնք գերիշխում են։ Տարածված պատճառներն են հսկայական union-ները (հազարավոր string literal-ներ), խորը ռեկուրսիվ conditional տիպերը և barrel ֆայլերը, որոնք ստիպում են ամեն մոդուլին import անել ամեն ինչ։ Repo-ն բաժանիր project references-ով. յուրաքանչյուր package ունի composite: true և declaration output, իսկ tsc -b-ն build է անում միայն փոխվածը՝ վերաօգտագործելով incremental build-երի .tsbuildinfo ֆայլերը։ isolatedDeclarations-ը (TypeScript 5.5) պահանջում է export-ների բացահայտ տիպեր, որպեսզի այլ գործիքներ կարողանան .d.ts ֆայլեր գեներացնել զուգահեռ՝ առանց checker-ը աշխատեցնելու։ Մեծ intersection-ների փոխարեն նախընտրիր իրար ընդլայնող interface-ներ, որովհետև կոմպիլյատորը interface-ների հարաբերությունները ավելի լավ է cache անում։ TypeScript 7-ի native կոմպիլյատորը սրա մեծ մասը շատ ավելի արագ է դարձնում, բայց նույն կառուցվածքային խորհուրդները դեռ գործում են։

bash
# Where does the time go?
npx tsc --noEmit --extendedDiagnostics
npx tsc --noEmit --generateTrace ./ts-trace
 
# Monorepo: build only what changed, in dependency order
npx tsc -b packages/api packages/web --verbose
 
# packages/shared/tsconfig.json (excerpt)
# "composite": true, "declaration": true,
# "incremental": true, "isolatedDeclarations": true

Իրական իրավիճակներ

01

Backend-ը վերանվանեց դաշտը, և production-ը կոտրվեց

Խնդիր

API-ն սկսեց վերադարձնել totalAmount՝ total-ի փոխարեն։ Frontend-ը նորմալ կոմպիլյացվեց, որովհետև պատասխանը տիպավորված էր cast-ով, և պատվերի էկրանը բոլոր հաճախորդներին NaN էր ցույց տալիս, մինչև hotfix-ը։

Ինչպես լուծել

Ցանցին վերաբերվիր որպես անվստահելի մուտքի։ Պատասխանը սահմանիր schema-ով (Zod կամ Valibot) և TypeScript տիպը ստացիր դրանից z.infer-ով, որպեսզի մեկ սահմանումը կառավարի երկուսն էլ։ Յուրաքանչյուր պատասխան parse արա մեկ API adapter շերտում. ձախողման դեպքում issue path-ը և raw payload-ը գրանցիր error tracking-ում և NaN-ի փոխարեն ցույց տուր հասկանալի սխալ։ Ավելի լավ է՝ տիպերը գեներացրու backend-ի OpenAPI փաստաթղթից CI-ում, որպեսզի վերանվանված դաշտը կոտրի frontend-ի build-ը deploy-ից առաջ։ Ավելացրու contract test, որը schema-ն աշխատեցնում է գրանցված իրական պատասխանի վրա։ as Order-ի նման cast-երն այդ դեպքում պետք է հանդիպեն միայն այդ adapter-ի ներսում։

02

Legacy կոդային բազա՝ strict ռեժիմում 3,000 տիպային սխալով

Խնդիր

Թիմն ուզում է strict ռեժիմ, բայց այն միացնելը հազարավոր սխալներ է տալիս հարյուրավոր ֆայլերում։ Ոչ ոք չի կարող մեկ ամսով դադարեցնել feature-ների վրա աշխատանքը՝ դրանք ուղղելու համար։

Ինչպես լուծել

strict-ը միացրու հիմնական tsconfig-ում, որպեսզի ամբողջ նոր կոդը համապատասխանի դրան, և առկա սխալները գրանցիր որպես baseline՝ միանգամից ուղղելու փոխարեն։ Script-ը աշխատեցնում է tsc, նորմալացնում output-ը ըստ ֆայլի և հաղորդագրության և CI-ը ձախողում է միայն այն սխալների դեպքում, որոնք baseline-ում չկան։ Ամեն անգամ, երբ որևէ մեկը դիպչում է ֆայլին, ուղղում է դրա սխալները, և baseline-ը փոքրանում է։ Քանակին հետևիր dashboard-ում։ Սկսիր բարձր արժեք ունեցող մոդուլներից (API շերտ, գումարի և պատվերների տրամաբանություն), իսկ հին, հազվադեպ խմբագրվող էկրանները թող վերջում։ Նոր any-ն և ts-ignore-ն արգելիր ESLint կանոնով՝ թույլ տալով ts-expect-error միայն գրավոր պատճառաբանությամբ։

03

Տիպերի ստուգումը CI-ում չորս րոպե է տևում

Խնդիր

API-ով, երկու web հավելվածով և ընդհանուր package-ներով monorepo-ն մեկ tsc է աշխատեցնում ամեն ինչի վրա։ CI-ը դանդաղ է, իսկ էդիտորը վայրկյաններով սառում է ընդհանուր package-ում յուրաքանչյուր փոփոխությունից հետո։

Ինչպես լուծել

Նախ չափիր --extendedDiagnostics-ով և --generateTrace-ով՝ տեսնելու, արդյոք ծախսը բաշխված է, թե առաջանում է մի քանի պաթոլոգիկ տիպերից։ Հետո repo-ն բաժանիր project references-ի. ընդհանուր package-ները ստանում են composite և declaration output, հավելվածները հղվում են դրանց, և CI-ը աշխատեցնում է tsc -b, որպեսզի չփոխված package-ները բաց թողնվեն cache արված .tsbuildinfo ֆայլերի շնորհիվ։ Հեռացրու ամբողջ package-ներ re-export անող barrel ֆայլերը, հսկա գեներացված union-ները փոխարինիր ավելի պարզ string տիպերով, որտեղ ճշգրտությունը պետք չէ, և export արվող ֆունկցիաներին ավելացրու բացահայտ return տիպեր, որպեսզի checker-ը package-ների միջև ավելի քիչ inference անի։ Դրանից հետո TypeScript 7-ի native կոմպիլյատորին անցնելը լրացուցիչ բազմապատկիչ է, ոչ թե միակ լուծումը։

Արագ ուղեցույց

Տիպերի ստուգում՝ առանց ֆայլեր գեներացնելու
npx tsc --noEmit
Գտնել դանդաղ տիպերը
npx tsc --noEmit --extendedDiagnostics --generateTrace ./ts-trace
Monorepo-ի incremental build
npx tsc -b packages/* --verbose
Branded id տիպ
type UserId = string & { readonly __brand: "UserId" };
Exhaustive switch-ի պաշտպանություն
default: { const _x: never = value; return _x; }
Տիպը ստանալ runtime schema-ից
type User = z.infer<typeof UserSchema>;
Պահպանել literal տիպերը config-ում
const config = { mode: "live" } as const satisfies Config;
Արգելափակել inference-ը մեկ արգումենտից
function pick<T>(items: T[], fallback: NoInfer<T>): T
Ռեսուրսի ավտոմատ dispose
await using conn = await pool.connect(); // needs Symbol.asyncDispose
Աշխատեցնել .ts ֆայլ Node.js-ի նոր տարբերակում
node src/script.ts // types stripped, not checked

Իմ իրական նախագծերում

  • Multi-tenant POS SaaS-ը, որը համատեղ ղեկավարում եմ, ամբողջությամբ TypeScript է՝ NestJS 11 API Prisma-ով 181 տվյալների բազայի մոդելների վրա, React 19 հավելվածներ, Electron դրամարկղ և Next.js կայք՝ մեկ monorepo-ում։
  • Կենդանիների ապրանքների խանութը աշխատում է Next.js 16-ով՝ TypeScript strict ռեժիմում, մոտ 600 TS/TSX ֆայլով։
  • Սուրճ պատվիրելու React Native հավելվածում post-edit hook-ը աշխատեցնում է tsc և ESLint և հաղորդում է միայն նոր սխալները՝ գրանցված baseline-ի համեմատ, այնպես որ տիպային պարտքը կարող է միայն նվազել։
  • Նույն հավելվածը backend-ի պատասխանները նորմալացնում է մեկ տիպավորված adapter շերտում, այնպես որ backend-ում դաշտի վերանվանումը երբեք չի դիպչում էկրանին։

Հարցեր և պատասխաններ

Q1Ո՞րն է any-ի և unknown-ի տարբերությունը։

Երկուսն էլ կարող են պահել ցանկացած արժեք, բայց վարվում են հակառակ ձևերով։ any-ն անջատում է տիպերի ստուգումը. դրա վրա կարող ես մեթոդներ կանչել, փոխանցել ցանկացած տեղ, և սխալները երևում են միայն runtime-ում։ Այն նաև տարածվում է, որովհետև այն ամենը, ինչ ստանում ես any-ից, նույնպես any է։ unknown-ը ապահով տարբերակն է. կոմպիլյատորը թույլ է տալիս դրանում ինչ ասես պահել, բայց չի թողնում օգտագործել, մինչև չնեղացնես typeof-ով, instanceof-ով, type predicate-ով կամ schema վալիդացիայով։ unknown-ն օգտագործում եմ ցանցից եկած JSON-ի, բռնված սխալների և երրորդ կողմի callback-ների համար, իսկ any-ն համարում եմ code smell, որին պետք է մեկնաբանություն՝ ինչու է այն այնտեղ։

Q2Բացատրիր event loop-ը։ Ինչո՞ւ է promise callback-ը աշխատում setTimeout(fn, 0)-ից առաջ։

JavaScript-ն աշխատում է մեկ thread-ի վրա՝ call stack-ով։ Ասինխրոն աշխատանքը պատվիրակվում է host-ին (բրաուզեր կամ Node.js), և ավարտվելիս callback է դրվում հերթում։ Event loop-ը հերթերից աշխատանք է վերցնում միայն երբ stack-ը դատարկ է։ Promise callback-ները և await-ի շարունակությունները գնում են microtask հերթ, իսկ թայմերները, I/O-ն և UI իրադարձությունները macrotask-ներ են։ Յուրաքանչյուր macrotask-ից հետո շարժիչը դատարկում է ամբողջ microtask հերթը՝ նախքան հաջորդ macrotask-ը վերցնելը։ Այսպիսով, ընթացիկ script-ի ավարտից հետո նախ աշխատում է resolved promise-ը, հետո՝ թայմերը։ Գործնական դասը. երկար սինխրոն աշխատանքն արգելափակում է ամեն ինչ, իսկ microtask-ների անվերջ շղթան կարող է «սովամահ անել» rendering-ը։

Q3TypeScript-ը կոդս runtime-ում ավելի արա՞գ կամ ավելի ապահո՞վ է դարձնում։

Ուղղակիորեն՝ ոչ։ TypeScript-ը ջնջվում է build-ի ժամանակ, այնպես որ աշխատող JavaScript-ը նույնն է, ինչ կգրեիր ձեռքով՝ առանց լրացուցիչ ստուգումների և առանց արագության աճի։ Օգուտը ավելի վաղ արձագանքն է. բագերի մի ամբողջ դաս (սխալ property անուններ, null-ի բաց թողնված մշակում, մոռացված union դեպքեր, արգումենտների սխալ հերթականություն) բռնվում է էդիտորում կամ CI-ում՝ production-ի փոխարեն։ Եզրերում իրական runtime ապահովության համար, ինչպես ձևերի մուտքագրումը, API պատասխաններն ու environment փոփոխականները, դեռ պետք է տվյալները վալիդացնել schema գրադարանով։ Համադրությունը լավագույնս աշխատում է, երբ schema-ն նաև ստեղծում է TypeScript տիպը, և ճշմարտության աղբյուրը մեկն է։

Q4Ի՞նչ են generic-ները և ե՞րբ կօգտագործես դրանք։

Generic-ները տիպի պարամետրեր են։ Դրանք թույլ են տալիս ֆունկցիային, class-ին կամ տիպին աշխատել բազմաթիվ տիպերի հետ՝ պահպանելով ճշգրիտ տիպային ինֆորմացիան։ any վերադարձնող fetch helper-ի փոխարեն getJson<T>-ն վերադարձնում է Promise<T>, և կանչողը ստանում է տիպավորված User կամ Order։ T extends { id: string }-ի պես սահմանափակումները ասում են, թե տիպը նվազագույնը ինչ պետք է պարունակի, ինչը թույլ է տալիս ֆունկցիային ապահով կարդալ item.id։ Generic-ներին դիմում եմ վերաօգտագործվող կառուցվածքային բլոկներում՝ API client-ներ, repository-ներ, ձևերի helper-ներ, ցուցակի կոմպոնենտներ և state store-եր։ Եթե ֆունկցիան միշտ մեկ տիպ է մշակում, generic-ը միայն աղմուկ է ավելացնում, ուստի այնտեղ չեմ օգտագործում։

Q5interface, թե type. ո՞րն օգտագործել։

Օբյեկտների կառուցվածքների համար դրանք գրեթե փոխարինելի են, և կոդային բազայում հետևողականությունն ավելի կարևոր է, քան ընտրությունը։ Interface-ները կարելի է ընդլայնել extends-ով և միավորել՝ երկու անգամ հայտարարելով, ինչը հարմար է գրադարանային տիպերը լրացնելու համար, օրինակ գլոբալ Window-ը կամ framework-ի request օբյեկտը։ Type alias-ներն ավելի ճկուն են. դրանք կարող են նկարագրել union-ներ, tuple-ներ, mapped և conditional տիպեր, ինչը interface-ները չեն կարող։ Իմ կանոնը պարզ է՝ type alias-ներ union-ների և հաշվարկվող տիպերի համար, interface-ներ public օբյեկտային պայմանագրերի համար, որոնք ուրիշները կարող են ընդլայնել։ Ինչ էլ ընտրես, խուսափիր ոճերը պատահականորեն խառնելուց, որովհետև review անողները ստիպված կլինեն մտածել՝ արդյոք տարբերությունը դիտավորյալ է։

Q6Ինչպե՞ս կներդնես TypeScript գոյություն ունեցող JavaScript նախագծում։

Աստիճանաբար։ Նախ ավելացրու tsconfig՝ allowJs-ով և անջատված checkJs-ով, որպեսզի TypeScript-ը կոմպիլյացվի առկա ֆայլերի կողքին՝ առանց սխալների։ Ֆայլերը վերանվանիր .ts մեկ մոդուլ առ մեկ՝ սկսելով «տերև» utility-ներից և API շերտից, որովհետև այնտեղի տիպերը հոսում են մնացած ամեն ինչի մեջ։ Նոր ֆայլերի համար strict-ը միացրու վաղ, նույնիսկ եթե հներին ժամանակավոր suppression-ներ են պետք, և հետևիր ts-expect-error մեկնաբանությունների քանակին, որպեսզի այն միայն նվազի։ Անմիջապես ավելացրու tsc --noEmit CI-ում, որպեսզի նոր տիպային սխալները հնարավոր չլինի merge անել։ Post-edit ստուգումը, որը baseline-ի համեմատ հաղորդում է միայն նոր սխալները, թիմին թույլ է տալիս առաջ շարժվել՝ առանց ամեն ինչ միանգամից ուղղելու։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս ես կիսում տիպերը backend-ի և մի քանի frontend-ների միջև, որպեսզի դրանք իրարից չշեղվեն։

Կա երեք հիմնական մոտեցում, և յուրաքանչյուրն ունի իր փոխզիջումները։ Monorepo-ում ընդհանուր TypeScript package-ը ամենապարզն է և ակնթարթային արձագանք է տալիս, բայց կապում է release-ների ցիկլերը և աշխատում է միայն այն դեպքում, երբ բոլոր սպառողները TypeScript են։ Տիպերի գեներացումը պայմանագրից, օրինակ՝ OpenAPI փաստաթղթից կամ GraphQL schema-ից, աշխատում է տարբեր լեզուների և տարբերակների միջև՝ codegen քայլի և այնպիսի պայմանագրի գնով, որը պետք է ազնիվ պահել. ես CI-ը ձախողում եմ, եթե գեներացված output-ը հնացած է։ tRPC-ի նման end-to-end տիպավորված RPC-ն լավագույն էրգոնոմիկան է տալիս, բայց client-ներին խիստ կապում է սերվերի մեկ իրականացման հետ։ Ինչ էլ ընտրես, ցանցի սահմանին միայն տիպերը բավարար չեն. ես միևնույն է պատասխանները վալիդացնում եմ runtime-ում մեկ adapter շերտում, որովհետև deploy արված backend-ը և հին մոբայլ client-ը կարող են իրար հետ չհամաձայնել՝ անկախ նրանից, թե ինչ է ասել կոմպիլյատորը։

S2Enum-ներ, string literal-ների union, թե const օբյեկտներ. ո՞րն ես ընտրում և ինչու։

Հավելվածի կոդի մեծ մասում ընտրում եմ string literal-ների union, կամ const օբյեկտ՝ դրանից ստացված union տիպով, երբ արժեքներն ինձ պետք են նաև runtime-ում, օրինակ՝ dropdown-ի համար։ Union-ները ամբողջովին ջնջվում են, բնականորեն serialize են լինում JSON-ի, լավ են նեղանում switch-երում և աշխատում են erasableSyntaxOnly-ի և Node-ի type stripping-ի դեպքում։ TypeScript-ի enum-ները runtime օբյեկտներ են գեներացնում, թվային enum-ներն ընդունում են ցանկացած թիվ, իսկ const enum-ները խնդիրներ են առաջացնում isolated compilation-ի դեպքում, ուստի դրանք վատ են տեղավորվում ժամանակակից toolchain-ներում։ const օբյեկտի pattern-ը տալիս է նույն առավելությունները, ինչ enum-ը՝ autocomplete, արժեքների մեկ աղբյուր, Object.values-ով իտերացիա, առանց թերությունների։ Միակ տեղը, որտեղ enum-ները դեռ հանդիպում են, հին կոդային բազաներն են, որտեղ առկա կոդի հետ հետևողականությունը կարող է ավելի կարևոր լինել, քան իդեալական ընտրությունը։

S3Որտե՞ղ է տիպային համակարգը դադարում օգնել, և ինչպե՞ս ես նախագծում այդ սահմանափակումները հաշվի առնելով։

Տիպերը նկարագրում են կառուցվածքը, ոչ թե վարքագիծը կամ ճշմարտությունը։ Դրանք չեն կարող ապացուցել, որ ցուցակը սորտավորված է, որ email-ն իրականում գոյություն ունի, որ գինը բացասական չէ, կամ որ արտաքին տվյալները համապատասխանում են իրենց հայտարարված տիպին։ Դրանք նաև դիտավորյալ անցքեր ունեն՝ cast-եր, any, covariant զանգվածներ և չստուգված ինդեքսային մուտք։ Ես սա հաշվի եմ առնում շերտերով։ Սահմաններում runtime schema-ները անհայտ մուտքը վերածում են վստահելի տիպերի։ Domain-ի ներսում branded տիպերը և smart constructor-ները կոդավորում են ինվարիանտներ, օրինակ՝ դրական գումարներ կամ վալիդացված id-ներ, այնպես որ անվավեր արժեքներ հնարավոր չէ ստեղծել՝ առանց ստուգում անցնելու։ Discriminated union-ները անթույլատրելի վիճակները դարձնում են աններկայացնելի։ Lint կանոնները արգելում են «փախուստի ելքերը» կամ դրանց համար մեկնաբանություն են պահանջում։ Իսկ մնացածը ծածկում են թեստերը՝ բիզնես կանոններ, հերթականություն, timing և ինտեգրացիա իրական ծառայությունների հետ։

Բառարան

Structural typing
Տիպերը համատեղելի են, երբ դրանց կառուցվածքները համընկնում են՝ անկախ այն անուններից, որոնցով հայտարարվել են։
Type erasure
Բոլոր տիպային անոտացիաները հեռացվում են build-ի ժամանակ, այնպես որ JavaScript-ի աշխատանքի պահին տիպերը երբեք գոյություն չունեն։
Narrowing
Կոմպիլյատորի կողմից լայն տիպի ճշգրտումը ավելի կոնկրետի՝ typeof-ի կամ switch-ի նման ստուգումից հետո։
Discriminant
Ընդհանուր literal դաշտ, օրինակ՝ kind, որը տարբերակում է union-ի տարբերակները։
Branded type
Primitive՝ համադրված միայն տիպային marker-ի հետ, որպեսզի նույն կառուցվածքով արժեքները չխառնվեն։
Conditional type
Տիպ, որը երկու արդյունքից մեկն է ընտրում՝ կախված նրանից, արդյոք մի տիպը extends է անում մյուսը։
Variance
Թե ինչպես է կոնտեյների կամ ֆունկցիայի subtyping-ը կապված դրա ներսի տիպերի subtyping-ի հետ։
Declaration file (.d.ts)
Ֆայլ, որը պարունակում է միայն JavaScript կոդի տիպային ինֆորմացիա և օգտագործվում է գրադարանների ու project references-ի կողմից։
Project references
Կոդային բազայի բաժանումն առանձին build արվող TypeScript նախագծերի, որպեսզի վերստուգվեն միայն փոխված մասերը։
Microtask
Հերթագրված աշխատանք, օրինակ՝ promise callback, որն աշխատում է նախքան event loop-ը հաջորդ task-ը վերցնի։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։