Галоўная / Артыкулы

Што компіляр TypeScript зафіксуе, а JavaScript працяўна паспускае

Порашчыце JavaScript і TypeScript парадульна: адгадванне типаў, анатазыі, прымітывы, any, з’еднанні і функцыі з адзначэнням типаў, а таксама тое, што на самай працэ практыкавання tsc фіксуе і выдае.

Выдавец: REACTAPP.TOP14 хв чытання3122 слоў

Крыніцы: JavaScript vs TypeScript: Understanding Type Safety

JavaScript можа выконваць практычна ўсё, што яму даюць, і самэ гэта ўсё чыста прычына таго, чаму дзеякіі багі прасвечваюць толькі пад час рэальнай експлуатацыі. TypeScript додае крок пераканальнага аналізу пры выконанні коду, але лёгка паспяліць неправильна зрозумець, што самэ гэты крок робіць і чым вон табе не захоўвае. У гэтым кяліку прадстаўлены маленькія, з можлівасцю атрыбутавання типаў, прыклады — ад перэзначэнняя зменных да функцый з атрыбутамі типаў, ёсць можласць точна пазначыць, дзе JavaScript застаёцца мовчкім, дзе компайлер TypeScript выказвае сваю дзейнасць, і як налаштаваць його так, каб бягі выконання фактычна блакаліся ў разе наявнасці бягаў.

Дынамічны типаванне проты статычнага типавання

JavaScript ўжоўцяе дынамічнае типаванне. Зменная не мае фіксованага типу; тип належыць той ценнай, яку ёна зараз мае, вылучаецца пад час выконання програмы і можа змяніцца ў будзь-які момант.

TypeScript — это статычна-тыпаваны суперсет JavaScript. Кожны правільны програма на JavaScript таксама ўзьмецца за правільны праграма на TypeScript з сынтаксічной точкі зору, але TypeScript дадае перакладчык типаў, який аналізуе ваш код у час кампайлявання, прытымкі які-небудзь код не будзе выкананы. Пасля чаго перакладчык удалівае інфармацыю пра типы і выдае звычайны JavaScript.

Усё іншае ў гэтым кяліку выхадзіць з гэтага аднаго разліку. Выбіранне типаў у час выканання робіць JavaScript гнучкым, але дазволяе помылкам застацца непазначанымі. Выбіранне типаў з самага пачатку робіць TypeScript строгейшым, але выявляе багато помылак рана, і самэ гэта маюць на увазе людзі, калі называюць TypeScript безпечным з точкі зору типаў. Строго кажучы, гэтая безпека — это гарантія ў час кампайлявання: яна дзейсніцца толькі настолькі, наскількі можа пазначыць перакладчык.

Калі зменяецца тип змэннай

У звычайным JavaScript пачніце з числовай змэннай і пераканаўцеся ў яе типе за дапамогою typeof:

let age = 30;
console.log(typeof(age));  // output is number

Чырвоныя лініі пазней, нічога не заважае вам зберагчы стрэлку ў той жа зменны:

age = "John";
console.log(age);
console.log(typeof(age)); // string

JavaScript прыме гэта без паведамленняў чы рошчэнняў. Першы typeof паказвае "number", а другі — "string": адна зменная, два несувязаныя типы за ўсё яе жыццё. У маленькаму скрыптэ такое нешкодна; у большай базе коду будь-які код, який яшчэ спакоюецца на ліквідабельнасць age як на числа, будзе працаваць некоректна далёка ад лініі, якая гэта спрычыніла.

TypeScript адмовяецца прыняць адпаведны код:

let data = 2;
data = "two"; // ❌ Error: Type 'string' is not assignable to type 'number'

Калі TypeScript бачыць let data = 2;, ён на адначасовай значэнні выводзіць, што data мае тип number, і заставае гэты тип пры всю трываласць жыцця зменнай. Гэта явісна называецца выводам типу. Таму прызначэнне "two" на наступным рядку адхоўвается кампайлярам раней, чым програма запускаецца.

У замяну на вывод типу, вы можаце самі указаць яго:

let data: number = 2;

: number — гэта синтаксіс, які існуе толькі ў TypeScript, а не ў JavaScript. Для безпекі типаў обе формы є эквівалентнымі; версія з анотацыяй проста робіць намер напісання коду видным для кожнага, хто чытае яго пазней, укладаючы ў гэта і вас за шасць месцаў.

Правіло, якое трэба запамятаць: JavaScript дозволяе зменіць тип змэннай столькі разоў, сколькі хочаце, тады як TypeScript фіксуе тип змэннай адразу, будзь то за дапамогою вашаеў анотацыяў чы першага значэння, і прыменяе яго даўжайша.

Пастка з роўнем і стрэлкай

Пярэдні прыклад паказвае, што TypeScript пераглядае типы. Наступны паказвае, чаму гэта важна.

JavaScript знову без працы дозволяе перазапісаць змэнную, якая мела роўень, значэннем у вигляде цалага:

let message = "Hello";
let num = 2;
message = 20; // totally allowed in JS

А тут класычныя неспадзенні:

let result = "5" + 3;
console.log(result); // "53", not 8

Вы можаце спакавацца, што будзе 8, таму што "5" выглядае як цыфра. Але "5" — это строка, і калі аператар + бачыць строкі з обох бакоў, JavaScript выконвае ўз’еднанне строкаў уместа арыфметычных вырахункаў. Ён ператварае 3 у текст і з’еднае яго з другой стракай, чыгунне "53". JavaScript выконвае гэта без жадных працоў, таму што ён ніколі не пераказвае типы заздалегідь; ён проста выконвае тое, што напісана ў кодзе у момент выконання.

Што робіць TypeScript з тым жа кодам

Ёсць версія на TypeScript, у якой типы чытальна паказаны:

let num1: string = "5";
let num2: number = 3;

А сама адыяція:

let add = num1 + num2; // expected 8, but num1 is a string and num2 is a number
console.log(add); // actual result is 53

Выдрукаваная значэньце яшчэ 53. Сюды саме часта памылка ў адказах, таму важна быць точным. TypeScript спецыяльна дазваляе string + number, таму што з’еднанне тексту з цяжэйкім лічыбой є распашчаным і дазволеным у JavaScript. Камплектар пераказоў не адхіляе такое выражэнне; ён выводзіць, што add мае тип string.

Памылка аператара, якую можна было пабачыць, выглядае так:

Operator '+' cannot be applied to types 'string' and 'number'.

TypeScript паведамляе такія паведамленні для камбінацый операндаў, якія ён сачытае беззначнымі, напрыклад, калі дадаёма boolean да number, або калі дадаёмась значэнні з типамі, якія не маюць нічага спакойнага. Гэта не тое, што адбываецца для num1 + num2 вышэй.

Тут TypeScript захоўвае вас, калі рэзультат выкарыстоўваецца як число. Якбы вы напісалі let add: number = num1 + num2;, кампайлер паведаміў бы, што тип string не можа быть прызначаны для типу number. Таксама, перадача add функцыі, якая чакае number, таксама выйдзе нявдалай. У практыцы самэлькі так і ловіцца баг "5" + 3 у кодзе з типаваннем: не пры знаку +, а там, дзе випадкова строка зустрэчаецца з кодам, для якога было обявлена ціль — число.

Чаму бягі пры злучэннях не зупіняюць вашу программу за значычным становішчам

Есць ўсё жа другая тонкасць. Нават калі tsc знаходзіць памылку типу, за значэнням прызначэння ён усё раве выдае скомпільаваны файл .js. Запуск гэтага файла за дапамою node выконвае звычайны JS, больш нічога. Уся інфармацыя пра типы была стыроўвана, і застаецца толькі логіка часу выконання.

Гэта сутнасць TypeScript: ён не зміняе спосабу працы вашага коду часу выконання. Ён адзвярожвае проблэмы ў вашам редактары і пад час компіляцыі, прычымоў як вы нічога не запускаеце. Якщо вы хачаце, каб компілятор адмовіўся выдаць результат калі є памылкі типу, увяжыце гэты параметр у tsconfig.json:

{
  "compilerOptions": {
    "noEmitOnError": true
  }
}

Калі параметр noEmitOnError ўвімкнуты, код зі спамою типа ніколі не доходзіць да node, таму што tsc зупіняецца без запісу файла JavaScript. У большасці рэальных проектаў той жа эфект досягаецца ў системах CI праз запуск tsc --noEmit як окольніка перагляду типаў, і будова проекта завершваецца з адмовай праз будь-яю спаму.

Ключоўы момент: ценнасць TypeScript не закладаецца ў іншую роботу пад час выканання, таму што скомпільаваны TypeScript — это JavaScript. Його ценнасць закладаецца ў выявленні несувязей прычымо да запуску, чым даецца можлівасць выправіць бягы, якія JavaScript бы працаваў без жадных паўтарэнняў.

Анотацыі і адгадванне

Этыя два тэрміны описваюць працоўнае спосабы, якія даходзяць да таго ж рэзультата. Разглянем анотаваную декларацыю:

let age: number = 30;

Працюем па шагох:

  • let — гэта ключавы слова для адзначэння
  • age — гэта назва зменнай
  • 30 — гэта значэнне, якое прызначаецца зменной
  • number — гэта тип
  • : number, тылдзе пасля типу, — гэта адзначэнне типу

Адзначэнне ясна паведамляе TypeScript, які тип мае зменная, выкарыстоўваючы синтаксі з тылдзе, такія як : number, : string або : boolean. Інферэнція — гэта працэс наадворот: вы не паводліваете тип, і TypeScript выважвае яго з прызначанага значэння.

let age = 30;        // inference — TS decides this is a "number" on its own
let city: string = "Delhi"; // annotation — you told TS explicitly

Оба спосабы даюць аднаковую захоўнасць, таму што як толькі тип установліваецца — чы то вам, чы компілятором — ён стае незменным. Анотацыі служаюць галоўная метай для павышэння яснасці. Яны найболей корыстныя, калі зменная прызначаецца без пачатковага значэння, адколі няма чаго выважваць, а таксама ў падписах функцый і публічных API, дзе яны задаюць мету ўжытку. Для локальных зменных з явным пачатковым значэнням большасць команд дазволяе системе самай выважваць значэння.

Прімітывныя та не-прімітывныя типы

Прыдатны спосаб разгрупаваць типы, з якімі вы зустроўцеся на пачатку:

  • Прімітывныя типы, такія як number, string, boolean, undefined і null, кожны з яных заўважна храніць адно значэння за раз.
  • Непрымітывныя, або структураваныя типы, такія як масівы, об’екты, туплы, функцыі, а таксама формы, якія описваюцца за дапамою класаў і інтэфейсаў, аб’еднваюць калькі значэнняя, напрыклад масів з рэзультатамі або запис прыўеднікання з іменем і электронным паштою.
  • Прымітывны тип — цэлая коробка, якая мае адну рэч; структураваны тип — цэлая скрывка, якая мае ў сабе багато коробак.

    У спісах для пачаткуючых часто паказваюцца ўсё тры конструкты разам з прымітывнымі типамі, і яны практычна описаны нижэй: any і типы-уніяны. Строга кажучы, жаданы з іх не ўжо прымітывны тип. any — цэлая альтернатыва, якая выключае перакантрольванне, а тип-уніяна — спосаб аб’еднавання іншых типаў. Яны знаходзяцца разам з прымітывнымі типамі, таму што як правило вжываюцца з адзінамі значэннямі.

    Прымітывныя типы ў практыцы

    Чысла

    У TypeScript тип number абаратваць цэлыя лікі і значэнні з плаваючай канмою. Не існуе окольных типаў int чы float, як у багацьых іншых мовах, таму што сам JavaScript зберагае звычныя лікі як 64-бітныя значэнні з плаваючай канмою.

    let age = 20;
    let price = 26.2;
    let big = 24757689;
    

    Выдрукав іх та пераканалізавшы адны з іх за дапамою typeof:

    console.log("Age :", age);
    console.log("price:", price);
    console.log(big);
    console.log(typeof(age)); // "number"
    

    Цэлыя лікі, дзесятковыя значэння і великія значэння ўсе проста являюцься number, і typeof паведамляе "number" для кожнага з іх. Для цэлых лікіў, якія выходзяць за межы дапустаемага дыапазону данай форматы з плаваючай канмою, JavaScript праставляе окремы тип bigint.

    Строкі

    Строка — это рэшта симвалаў: слова, запаведзе нябо будзь-яй текст. Строковыя літералы можна запісаць з апошнімі ці двойнымі лапкамі, або з обратными лапкамі.

    let firstname: string = 'JOHN';
    let lastname: string = "SINGH";
    console.log("Hello", firstname, lastname); // Hello JOHN SINGH
    

    Калі передаецца калькі аргументаў, роздзеленых комамі, у console.log, яны выведаюцца з разлікам прасоў, таму імя выведаецца цэлым. Аднак супадзелены лінійкі не выкаанае таго, што здаецца:

    // let greeting: string = "Hello", firstname, lastname; // ❌ this does NOT concatenate
    

    У выказванні let кома викорыстоўваецца для роздзелення калькілявань; яна не з’едынае текст. У гэтай лініі прызначаюцца тры разлучныя зменныя — greeting, firstname і lastname, прычаму толькі greeting атрымле значэнне. Якщо firstname і lastname вялікі ў там самым дыапазоне, TypeScript таксама паведаміць пра тое, што яны перапрызначаюцца.

    Такім чынам, комы падходзяць для выведання калькілявань, але каб зберагчы саедыненую фразу ў зменную, трэба викорыстоўваць шаблонны літерал: строку, абгорнутую ў backticks, з месцарамі ${} для выразаў.

    let firstname: string = 'JOHN';
    let lastname: string = "SINGH";
    let greeting: string = `Hello ${firstname} ${lastname}`;
    console.log(greeting); // Hello JOHN SINGH
    

    Шаблонныя літэралы — это рекамендаваны спосаб працы з текстам і значэннямі. Яны выглядаюць чыстае, чым ланцужкі з’еднання за дапамою +, і таксама падтрымляюць строкі, якія распрастаюцца на калькі ліній.

    Булевыя значэння

    Булева зменная мае роўна два можлівыя значэння: true і false.

    let isStudent: boolean = true;
    let hasJob: boolean = false;
    

    Запіс вады і пераконтрацея яе типу:

    console.log("Is student?", isStudent); // true
    console.log(typeof (isStudent)); // "boolean"
    

    Булевы значэнні керуюць кожным умовным вырачэннем у вашам коде: оператырамі if, умовамі циклаў і ўсюдзе, дзе прыменяецца рашэнне «так» або «нет».

    Null і undefined

    Як null, так і undefined парадзіваюць абсэнцыю значэння, але яны маюць разную прыначу. Пачніце зяўжо з мэньбера, які быў адзначаны, але ніколі не прызначаны:

    let emptyvalue;
    console.log(typeof emptyvalue); // "undefined"
    

    Заявленная, але не прызначаная зменная мае значэнне undefined, што означае, што зменная існуе, але ў яй паказаўся нічога.

    Вы таксама можете явна прызначыць зменным типы null і undefined:

    let emptyvalue: null = null;
    let value: undefined = undefined
    

    Потым парабялейце, як кожны з іх выдрукаваецца, а таксама зменную, яка была запісана, але не прызначана:

    console.log(emptyvalue); // null
    console.log(value);      // undefined
    let price: number;
    console.log(price); // undefined - declared, but not yet assigned any value
    

    У простыях словах:

    • undefined означае, што зменная існуе, але ў яй не была задана значэнне; гэта стандартны порожні стан.
    • null означае, што разработчык намеравана задаў зменную на „жаднае значэнне“; гэта намераваная, явная порожнеча.

    Анотацыя типу, такая як : number, обмежвае тое, што зменная можа мячыць пазней; яна не задае пачатковае значэнне. Таму пад час выконання price выводзіць undefined. Протыма, калі ўвімкнуты режым strict, які є стандартам у сучасных проектах, TypeScript паведамляе пра адказ на рэчы console.log(price), таму што зменная выкарыстоўваецца раней, чым ёй было прызначана значэнне. Гэтыя пераконтроўкі ёсць ўжо адным з прыкладаў таго, як кампайлер выявляе пакрыткі, якія звычайны JavaScript дазволіў бы.

    У звычным коде вы рэдка калі атрыбутуеце зменную як чыста null або undefined; гэтыя прыклады існуюць для таго, каб паказаць, як паводзіцца з гэтымі двума станамі пустоты. Набаго частэй викорыстоўваецца з’еднанне на кшталт string | null для значэння, якое можа быць відсутняе.

    Тып any: выключэнне перакрытчика

    Выкорыстоўвайце any рэдка і свядома, адтакуль ён выключае саме тыя перакрыцці, якія былі введаны ў TypeScript. Зменная any прымае любы тип значэння і можа быть перазначаная на іншы тип за бажаннем, абсолютна як у звычным JavaScript.

    let value: any = "abcd";
    console.log(typeof value); // "string"
    

    Перазначыце яго на число, а пасля — на логічную значэнне:

    value = 123;
    console.log(typeof value); // "number"
    value = true;
    console.log(typeof value); // "boolean"
    

    Зменная пераходы з масцовага тексту на число, а потым на логічнае значэнне без жадных працоў, таму што аннатасція паведаміла кампайляру не перакантраваць яе.

    Існуюць легітымныя способы ўжыцця. Калі вы сапраўды не можете знайсці тип заздалегідь, напрыклад, калі маюцца непераканальныя зовнішнія даны, або пад час поступовай міграцыі з JavaScript, калі немагчыма запісаць усё заодно, any дапамагае вам працаваць далей. Аднак частыя апоўзкі на яго коштуюць вам функцыі автодаполнення, паказу працэсу компілявання і захоўнай сеткі, якая спачатку стала прычыной викорыстоўвання TypeScript. Вашыяце яго як апошню можлівасць, а не як прычыну для звыку. Для дадзеных невядомага формата unknown зазвычай є кращым выборам, таму што ён змушвае вас пераканацца ў значэнні пры яго викорыстоўванні. Наша інструкцыя па замене any на безпечныя за типам патэрны детальна розглядае альтернатывы.

    Тыпы-уніяны: кантролюемы сэт вароў

    Тып аб’юнкціі дазваляе значэнню быть адним з кальколькох спецыяльных типаў. На адзін момент з any, ён не прымае все, а толькі тыя типы, якія вы пераказалі. „Union“ — гэта не ключавая слова; це назва шаблона, якая пішыцца з символам трубкі |.

    let id: number | string | boolean;
    

    Парадульна прызначэнне кожнага дазволенага типу:

    id = "123abc";
    console.log(id);
    id = 12;
    console.log(id);
    id = true;
    console.log(id);
    

    Зменная id можа мячыць чысла, строка або булева значэнне, і нічога больш. Прабава прызначыць масэвую структуру, такую як [1, 2, 3], адразу ж адхоўвана, таму што масэвыя структуры не ўскладневаюць групавання. Гэта дае вам гнучкасць самэльку там, дзе яя неабходна; рэальныя ідентыфікаторы, напрыклад, іноды прыходзяць у вастоце чысл, а іноды — строкаў, пры тым застаёмацца чыткім межам для таго, што дазволена. Перш чым вы вжываеце значэнне з групавання спецыфічным для тыпу спосабам, TypeScript выкарыстоўвае ваша прабаву яго сузіць, напрыклад, за дапамой пераканання typeof.

    Тыпаванне функцый

    Функцыі аб’еднваюць адна задача ў код, які можна перывжываць. Вы яе адзін раз задаёте і вызываеце ёю там і столькі разоў, сколькі яя неабходна.

    Функцыя без параметраў

    Найпростейшая версія жорстка задае сваея значэння:

    function sum() {
      console.log(10 + 20);
    }
    

    Апраначэнне функцыі не запускае яе. Нічога не выходзіць, пакуль вы яе не вызываеце:

    sum(); // calling the function → prints 30
    

    Сама гэта раздзеленасць і ўсё: напісаць логіку адназначна і выканаць яе за патрэбай.

    Дадзенне параметраў з типам

    Функцыі становяцца справжна повторна викорыстоўванымі, калі яны прыймаюць параметры — заместнікі, якія дазволяюць тому ж коду працаваць з разнымі значэннямі праз кожны вызов.

    function sum(x: number, y: number) {
      console.log(x + y);
    }
    

    Выкліканне за дапамою двух номераў:

    sum(10, 20); // prints 30
    

    Тепер sum большэй не прыяжаны да 10 і 20; sum(5, 7) чы sum(100, 1) працуюць таксама добра. : number пасля кожнага параметра — это анотацыя типу для параметра, таму TypeScript адхроюе такі вызов, як sum("hello", 20), прычыму коду запускацца. Самэлькі TypeScript зазвычай практыкуе сваю корыстнасць — на межах функцый, дзе адна частка програмы перадае данні іншай.

    Эта версія як і раней выведае свой рэзультат толькі за дапамою console.log. Яна нічога не вяртае таму, хто яе вызваў, а гэта мае значэнне для наступнага крока.

    Аклараванне типу вярнення

    Функцыя, якая вяртае свой рэзультат, ў дзесяць разоў корыстнейшая:

    function sum(x: number, y: number): number {
      return x + y;
    }
    

    Захаванне і выкарыстоўванне вернутага значэння:

    let res: number = sum(10, 20);
    console.log(res); // 30
    

    Тры ўсёбыткі змяніліся па супорэднанню з пакульным варыянтом:

    • return x + y; вяртае значэнне званіку, у працоўнасці замест таго, каб яго вывести ўнутрь функцыі.
    • : number пасля спісу параметраў, у (x: number, y: number): number, являе сабою анацэнку типу вяртання. Яна гарантуе, што функцыя завжды вяртае число, а вяртанне строкі будзе прычыной памылкі компілявання.
    • Паколькі sum тепер вяртае значэнне, let res: number = sum(10, 20); можа яго зберагчы для пазнейшага выкарыстання: запісаць яго, перадаць іншай функцыі або сумаваць з іншымі рэзультатамі.

    Функцыя, якая вяртае свой рэзультат, можа быць складаная з іншай логікі; тая, якая толькі фіксуе падзеі, ўосабліваяць мёртвы канец, чыя выходна значэння не можа быць перызначана. TypeScript часта можа вывучыць типы вяртання, але яўны ўпіс іх у экспортаваныя функцыі заходзіць ад таго, каб нечакана не змяніцца тое, што функцыя вяртае.

    Узагальненне

    • JavaScript выявляе типы пад час выканання і абсалютна даверяе вам, што ўскладнюе роботу, але дазволяе наявнасць багоў, такіх як "5" + 3, які дае "53".
    • TypeScript выкананае за дапамою анотацый чыя вывучэнням типу кожнай зменной і пераканаецца ў яго пад час кампілявання, першы чым код запускаецца.
    • TypeScript не адхоўляе кожную сумнівную выразу; з’едыненне строк і цалых лічбаў дазволена, а захопленне выкананае тады, калі рэзультатныя строкі патрапляюць у код, які чакае цалое лічба.
  • Парадоксы типу за значычным становішчам не спяшаюць выходу дадзеных; ўжывайте noEmitOnError або параметр tsc --noEmit у процесе CI, каб яны блакавалі запуск будовы.
  • Для справжніх невядомых значэнняў застаўляйце any, для большай гнучкасці вяртайцеся да з’еднанняў і unknown, а таксама адзначайце параметры функцый і типы вярнення там, дзе даныя пераходзяць межы.
  • Рэзультат выканання коду на JavaScript і TypeScript ёсць ідэнтычны. Разлік крыўцяе тады, калі вы ведаеце пра проблему: у вашым рэдагары, а не перед вашымі корыстнікамі.