Ֆունկցիաների ծանրաբեռնումը Typescript-ում

Ֆունկցիաների ծանրաբեռնումը Typescript-ում

Եթե դուք ունեք աշխատանքային փորձ որևէ տիպիզացված լեզվի հետ, ապա ձեզ ծանոթ է ֆունկցիաների ծանրաբեռնման(գերբեռնման, անգլ.՝ overloading) կոնցեպտը: Եթե՝ ոչ, ապա հակիրճ հիշեցնենք նրա էությունը.

Երկու ֆունկցիաներ կամ մեթոդներ կարող են ունենալ միևնույն անունը, եթե նրանց արգումենտների ցուցակները տաբեր են. այսինքն՝ մենք կարող ենք երկու ֆունկցիայի(մեթոդի) տալ միևնույն անունը, եթե նրանք ընդունում են տարբեր քանակությամբ արգումենտներ, կամ տարբերբում են ընդունվող արգումենտների տիպերը: Ծանրաբեռնված ֆունկցիայի կանչի դեպքում կիրականացվի տվյալ ֆունկցիայի կոնկրետ իրագործումը՝ կախված կոնտեքստից, հնարավորություն տալով ֆունկցիաների կամ մեթոդների տարբեր տրամաբանությունների իրագործում՝ կախված ընդունվող արգումենտների կոնտեքստից:

Ֆունկցիոնալ տեսանկյունից սա հրաշալի է: Իսկ արդյո՞ք մենք այն կարող ենք օգտագործել նաև JavaScript-ում, հաշվի առնելով, որ վերջինս տիպիզացված լեզու չէ: Իսկ ի՞նչ կարելի է ասել TypeScript-ի դեպքում: Արդյո՞ք այն լռելյայն սպասարկում է ծանրաբեռնումները: Կան արդյո՞ք թերություններ:

Հոդվածում մենք կպատասխանենք վերը թվարկված բոլոր հարցերին, իսկ երբ կպարզաբանենք հիմնականը, կդիտարկենք նաև դրանց կիրառման լավագույն փորձը:

Ծանրաբեռնումը JavaScript-ում

Թույլատրում է արդյո՞ք Javascript-ը ֆունկցիաների ծանրաբեռնում: Խիստ ասած՝ ոչ: Մենք կարող ենք նմանատիպ ինչ-որ բան իրագործել, բայց նման մոտեցմանն ունի մի շարք թերություններ: Թե ինչպիսիք՝ կիմանանք, դիտարկելով մի քանի օրինակներ:

Ենթադրենք, անհրաժեշտ է ստեղծել concatString ֆունկցիա, որը կընդունի 1-3 տողային պարամետրեր, և նրանք միացնի միմյանց՝ արդյունքում վերադարձնելով միացված տողը:

function concatString(s1, s2, s3) {
  let s = s1;
  if(s2) {
    s += `, ${s2}`;
  }
  if(s3) {
    s += `, ${s3}`;
  }
  return s;
}

concatString('one'); // կտպի 'one'
concatString('one','two'); // կտպի 'one, two'
concatString('one', 'two', 'three'); // կտպի 'one, two, three'

Կարծես թե այն է, ինչ ակնկալվում էր: Իսկ ի՞նչ տեղի կունենա, եթե կանչենք վերը նշված ֆունկցիան հետևյալ կերպ:

concatString('one', true);

Անշուշտ, ֆունկցիան կաշխատի, բայց կվերադարձնի սխալ արդյունք: Մենք իրականացնում ենք ստուգում ըստ գոյության, իսկ true-ն նման ստուգումն անցնում է, հետևաբար, մենք պետք է նաև ստուգենք արգումենտների տիպը: typeof-ի միջոցով ստուգենք արգումենտների տող լինելն ու այնուհետև իրականացնենք միացում՝ կոնկատինացիա:

if(s2 && typeof s2 === 'string')

Այս կերպ, տարբեր տիպերի հետ կապված խնդիրը վերացվեց: Իսկ ի՞նչ կլինի, եթե ֆունկցիան կանչենք ավելի շատ մուտքային արգումենտներով, քան նախատեսել ենք՝ տվյալ դեպքում, 3-ից ավելի արգումենտներով: Պարզապես ֆունկցիան կմշակի առաջին երեք արգումենտները՝ անտեսելով հաջորդները: Որպես արագ լուծում, մենք կարող ենք ստուգել arguments-ների length-ը, և վերադարձնել սխալի հաղորդագրություն, եթե այն գերազանցի նախատեսված արժեքը, մեր դեպքում՝ 3:

function concatString(s1, s2, s3) {
  if (arguments.length > 3) {
    throw new Error('signature not supported');
  }
  let s = s1;
  if(s2 && typeof s2 === 'string') {
    s += `, ${s2}`;
  }
  if(s3 && typeof s3 === 'string') {
    s += `, ${s3}`;
  }
  return s;
}

Նայելով այս օրինակին, կարելի է հասկանալ, թե ինչու է Javascript համայնքը նախընտրում խուսափել ծանրաբեռնումներից: Մեթոդների և ֆունկցիաների աճի և մեծացմանը զուգահեռ ավելի ու ավելի է դժվարանում դրանց կարդացելիությունն ու սպասարկումը: Իսկ սխալները հայտնաբերվում են միայն բուն իրականացման ժամանակ, այսինքն կամպիլյատորը չի կարողանա զգուշացնել հավանական սխալների մասին:

TypeScript-ը կգա օգնության

TypeScript-ը “ՍՏՈՊ” է ասում դինամիկ ծրագրավորման այս երկիմաստություններին ու գլխացավանքներին: Ձեռքի տակ ունենալով տիպիզացված մեթոդներ, միշտ միանշանակ է, թե ինչ է ակնկալում և վերադարձնում ֆունկցիան: Արդյո՞ք այն ծանրաբեռնումների լռելյայն սպասարկում ունի: Այո! Բայց մեզ համար սովորականից փոքր-ինչ տարբերվող ձևով:

TypeScript-ը կարող է օգնել միայն խմբագրման և հավաքման փուլում, քանի որ կոդը ի վերջո կվերածվի JavaScript-ի: Գործարկման ժամանակ մենք կունենանք նույն գործիքները, ինչ JavaScript-ում: Թեև սա իդեալական տարբերակ չէ, բայց ֆայլերը տիպիզաման միջոցով կարգի բերելու համար բավականին համապատասխան է:

Վերադառնանք նախորդ օրինակին: Չնայած այն այնքան պարզ է, որ կարիք չկա օգտագործել գերբեռնվածության սինտաքս: Պարզապես բավարար կլինի պարամետրերը հայտարարել ոչ պարտադիր.

function concatString(s1: string, s2?: string, s3?: string) {
  let s = s1;
  if(s2) {
    s += `, ${s2}`;
  }
  if(s3) {
    s += `, ${s3}`;
  }
  return s;
}

// ❎  սա կաշխատի
concatString('one');
concatString('one','two');
concatString('one', 'two', 'three');

// ❌ իսկ այստեղ կամպիլյատորը սխալ կվերադարձնի
concatString('one', true);
concatString('one', 'two', 'three', 'four');

Բարդեցված սցենար

Ի՞նչ անել, եթե ձեզ անհրաժեշտ է ինչ-որ ավելի կոնկրետ բան անել: Ասենք, արգումենտների հիման վրա վերադարձնենք այլ տիպի արժեք։ Միայն վերը նշված տեխնիկան կարող է բավարար չլինել: Դիտարկենք օրինակ.

function helloWorld(s?: string) {
  if (!s) {
    return Math.random();
  }
  return s;
}

// x կունենա string | number տիպ
const x = helloWorld('test');

Վերը նշված օրինակում մեթոդի վերադարձվող տիպը կլինի string | number: TypeScript-ը դուրս կբերի հնարավոր բոլոր վերադարձվող արժեքների տիպերը և կմիացնի դրանք հատուկ սինտաքսի միջոցով(|):

Մեթոդից ակնհայտ է, որ եթե արգումենտը string տիպի է, ապա վերադարձվող արժեքը ևս string է, հակառակ դեպքում՝ number: Իսկ եթե դա արտահայտենք ավելի կոնկրետ տիպերի միջոցով?: Կարելի է կիրառել ծանրաբեռնման սինտաքսը արգումենտի տիպը վերադարձվող արժեքի տիպին համապատասխանեցնելու համար:

Արդյունքը կլինի.

function helloWorld(): number;
function helloWorld(s: string): string;
function helloWorld(s?: string) {
  if (!s) {
    return Math.random();
  }
  return s;
}

// ❎ x -ը string տիպի է
const x = helloWorld('test');
// ❎ y-ը number տիպի է
const y = helloWorld();

Այժմ, ֆունկցիային string տիպ փոխանցելուց մենք ելքում կստանանք string տիպ, իսկ արգումենտի բացակայության դեպքում՝ number: Եկեք հասկանանք, թե ինչպես է աշխատում սինտաքսն այս պարագայում:

  • function helloWorld(): number: առաջին ծանրաբեռնում.
  • function helloWorld(s: string): string: երկրորդ ծանրաբեռնում.
  • function helloWorld(s?: string): Հիմնական ֆունկցիան, որը պետք է ընդունի նախկինում հայտարարված բոլոր ծանրաբեռնումները: Այն պետք է համապատասխանի բոլոր վերը հայտարարված վերադարձվող արժեքների տիպերին: Տվյալ դեպքում արտաբերվում է վերադարձվող արժեքների string | number տիպ:

Ի՞նչ կլինի անհամապատասխան տիպերով արգումենտների օգտագործման պարագայում: TypeScript-ը պարզապես չի կամպիլյացնի կոդը և սխալի մասին զգուշացում կգեներացնի:

function helloWorld(): Date;
//       ^^^^^^^^^^^
// ❌ Error: Cannot invoke an expression whose type lacks a call signature.
function helloWorld(s: string): string;
function helloWorld(s?: string) {
  if (!s) {
    return Math.random();
  }

  return s;
}

Միշտ նկատի ունեցեք գերբեռնվածության հայտարարման հերթականության կարևորությունը: Հիմնական ֆունկցիան պետք է հայտարարված լինի ամենավերջինը: Դիմենք TypeScript-ի դոկումենտացիային՝ մեկնաբանող ինֆորմացիայի համար.

Ճիշտ տեսակի ստուգման ընտրության ժամանակ կոմպիլյատորն անցնում է JavaScript-ի հիմքում գտնվող գործընթացին նման ուղի: Այն ստուգում է գերբեռնվածությունների ցանկը և, սկսած առաջինից, փորձում է ֆունկցիան կանչել տվյալ պարամետրերով։ Համընկնումը գտնելու դեպքում տվյալ գերբեռնվածությունը գնահատվում է որպես ճիշտ և ընտրում է այն։ Այս առումով ընդունված է կազմակերպել ծանրաբեռնվածություններ՝ շարժվելով ամենասպեցիֆիկից դեպի ամենաքիչ սպեցիֆիկը:

Սրանում համոզվելու համար փորձենք դիտարկել օրինակ, որտեղ հերթականությունը պահպանված չի լինի: Պատրաստ եղեք զգալի սխալների.

function helloWorld(): number;
//       ^^^^^^^^^^^^
// ❌ Error: Cannot invoke an expression whose type lacks a call signature.
function helloWorld(s?: string);
//       ^^^^^^^^^^^^
// ❌ Error: Cannot invoke an expression whose type lacks a call signature.

function helloWorld(s: string): string {
  if (!s) {
    return Math.random();
    //     ^^^^^^^^^^^^^^
    // ❌ 'number' տիպը չի կարող վերագրվել 'string' տիպի
  }

  return s;
}

Ինչպես երեւում է դոկումենտացիայում, կարգը պետք է ավելի սահմանափակողից դառնա ավելի քիչ սահմանափակող: Մեր նախորդ բազային մեթոդը՝ helloWorld(s: string) ֆունկցիան ամենաքիչ սահմանափակողն է: Դրա համար այն պետք է լինի վերջինը:

Պարամետրերի մի քանի տիպերի միավորումը

Ինչպես արդեն տեսանք, մենք հնարավորություն ունենք փոխելու վերադարձվող արժեքի տիպը՝ կախված արգումենտի պարամետրերից։ Միակ սահմանափակումն այն է, որ հիմնական ֆունկցիան պետք է համապատասխանի տիպի բոլոր տարբերակներին:

function foo(arg1: number, arg2: number): number;
function foo(arg1: string, arg2: string): string;
function foo(arg1: string | number, arg2: string | number) {
  
// ❎ x-ը string տիպի է
const x = foo('sample1', 'sample2');
// ❎ y-ը number տիպի է
const y = foo(10, 24);

// ❌ Error: No overload matches this call.
const a = foo(10, 'sample3');
// ❌ Error: No overload matches this call.
const b = foo('sample3', 10);

Ուշադրություն դարձրեք, որ arg1 և arg2 պետք է լինեն string | number տիպի՝ նախորդ ծանրաբեռնումներում նշված հնարավոր տիպերին համապատասխանելու համար:

⚠️ Մի փոքրիկ նյուանս ևս, որը պետք է նկատի ունենալ: TypeScript-ը չի կարողանա պարամետրերի հիման վրա ֆունկցիայի մարմնում տիպերը դուրս բերել: Սա նշանակում է, որ նույնիսկ եթե ֆունկցիայի մարմնում նշենք, որ arg1-ը տող է, TypeScript-ը չի որոշի, որ arg2 նույնպես պետք է լինի տող։

Խորհուրդներ՝ Ֆունկցիաների ծանրաբեռնման հետ աշխատելու համար

Այսպիսով, TypeScript-ում ֆունկցիայի գերբեռնվածության կոնցեպտները մեզ պարզ են, եկեք դիտարկենք դրա հետ աշխատելու մի քանի խորհուրդներ.

1. Խորհուրդ չի տրվում մի քանի ծանրաբեռնում գրել, որոնք կտարբերվեն միայն մուտքայի պարամետրերով

// ❌ սրա փոխարեն
interface Example {
  foo(one: number): number;
  foo(one: number, two: number): number;
  foo(one: number, two: number, three: number): number;
}

// ❎ կանենք այսպես
interface Example {
  foo(one?: number, two?: number, three?: number): number;
}

Ինչպես տեսնում եք, երկու ինտերֆեյսներն էլ նույն նշանակությունն ունեն, բայց առաջինն, ի տարբերություն երկրորդի, ավելի բարդ է ձևակերպված։ Այս սցենարում միանգամայն տեղին է ոչ պարտադիր պարամետրերի հայտարարումը:

2. Խորհուրդ չի տրվում ծանրաբեռնում գրել, որը կտարբերվի միայն մեկ արգումենտի տիպով

// ❌ սրա փոխարեն
interface Example {
  foo(one: number): number;
  foo(one: number | string): number;
}

// ❎ կանենք այսպես
interface Example {
  foo(one: number | string): number;
}

Նախորդ օրինակի նման, այստեղ էլ երկու տարբերակներն էլ միևնույն նշանակությունն ունեն, մինչդեռ երկրորդ տարբերակն առավել մատչելի է ձևակերպված: Տիպերի միավորմումն էլ այստեղ է առավել նախընտրելի լուծում:

Սկզբունքային օրենքն այն է, որ չպետք է անտեղի բարդացնել կոդը: Ծանրաբեռնման սինտաքս ավելացնել պետք է միայն այն դեպքերում, եթե այն իսկապես կբարձրացնի կոդի էֆեկտիվությունը:

Հետևություններ

Ֆունկցիաների գերբեռնումը TypeScript-ի կարևոր և օգտակար գործառույթներից է: Սակայն դա չարաշահել չի կարելի։ Ինչպես արդեն տեսել ենք, երբեմն կարող ենք գլուխ հանել նույնիսկ optional մոդիֆիկատորի շնորհիվ: TypeScript-ում ֆունկցիայի գերբեռնվածությունը օգնում է մաքրել կոդը. ճիշտ օգտագործման դեպքում կոդը դառնում է ավելի ընթեռնելի և ավելի հեշտ սպասարկվող:

Սկզբնաղբյուր՝ Mastering Function Overloading in TypeScript

1927