TypeScript vs. JavaScript: Twój przewodnik podstawowy

Opublikowany: 2022-03-11

TypeScript czy JavaScript? Deweloperzy rozważają ten wybór w przypadku projektów internetowych typu greenfield lub Node.js, ale jest to pytanie warte rozważenia również w przypadku istniejących projektów. Nadzbiór JavaScript, TypeScript oferuje wszystkie funkcje JavaScript oraz kilka dodatkowych korzyści. TypeScript wewnętrznie zachęca nas do czystego kodu, dzięki czemu kod jest bardziej skalowalny. Jednak projekty mogą zawierać tyle zwykłego kodu JavaScript, ile nam się podoba, więc używanie TypeScript nie jest propozycją typu „wszystko albo nic”.

Związek między TypeScript i JavaScript

TypeScript dodaje jawny system typów do JavaScript, co pozwala na ścisłe wymuszanie typów zmiennych. TypeScript przeprowadza kontrolę typu podczas transpilacji — jest to forma kompilacji, która konwertuje kod TypeScript na kod JavaScript w przeglądarkach internetowych i zrozumieniu Node.js.

Przykłady TypeScript vs. JavaScript

Zacznijmy od prawidłowego fragmentu kodu JavaScript:

 let var1 = "Hello"; var1 = 10; console.log(var1);

Tutaj var1 zaczyna się jako string , a następnie staje się number .

Ponieważ JavaScript jest tylko luźno typowany, możemy przedefiniować var1 jako zmienną dowolnego typu — od ciągu znaków do funkcji — w dowolnym momencie.

Wykonanie tego kodu daje wyjście 10 .

Teraz zmieńmy ten kod na TypeScript:

 let var1: string = "Hello"; var1 = 10; console.log(var1);

W tym przypadku deklarujemy var1 jako string . Następnie próbujemy przypisać do niego liczbę, co nie jest dozwolone przez ścisły system typów TypeScript. Transpilacja skutkuje błędem:

 TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10;

Gdybyśmy poinstruowali transpiler, aby traktował oryginalny fragment kodu JavaScript tak, jakby był to TypeScript, transpiler automatycznie wywnioskowałby, że var1 powinna być string | number string | number . Jest to typ unii TypeScript , który pozwala nam w dowolnym momencie przypisać var1 string lub number . Po rozwiązaniu konfliktu typów nasz kod TypeScript transpiluje się pomyślnie. Wykonanie go dałoby ten sam wynik, co przykład JavaScript.

TypeScript vs. JavaScript od 30 000 stóp: wyzwania dotyczące skalowalności

JavaScript jest wszechobecny, napędza projekty wszystkich rozmiarów, stosowany w sposób, który byłby niewyobrażalny w latach dziewięćdziesiątych. Chociaż JavaScript osiągnął dojrzałość, nie radzi sobie z obsługą skalowalności. W związku z tym programiści zmagają się z aplikacjami JavaScript, które urosły zarówno pod względem wielkości, jak i złożoności.

Na szczęście TypeScript rozwiązuje wiele problemów związanych ze skalowaniem projektów JavaScript. Skoncentrujemy się na trzech najważniejszych wyzwaniach: walidacji, refaktoryzacji i dokumentacji.

Walidacja

Polegamy na zintegrowanych środowiskach programistycznych (IDE), które pomagają w zadaniach, takich jak dodawanie, modyfikowanie i testowanie nowego kodu, ale IDE nie mogą sprawdzać poprawności odwołań do czystego JavaScript. Łagodzimy to niedociągnięcie, uważnie monitorując kod, aby uniknąć literówek w zmiennych i nazwach funkcji.

Skala problemu rośnie wykładniczo, gdy kod pochodzi od strony trzeciej, gdzie uszkodzone odwołania w rzadko wykonywanych gałęziach kodu mogą łatwo pozostać niewykryte.

W przeciwieństwie do TypeScript, możemy skoncentrować nasze wysiłki na kodowaniu, mając pewność, że wszelkie błędy zostaną zidentyfikowane w czasie transpilacji. Aby to zademonstrować, zacznijmy od starszego kodu JavaScript:

 const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } }

.toISO() jest literówką metody moment.js toISOString() , ale kod zadziała, pod warunkiem, że argument format nie jest ISO . Gdy po raz pierwszy spróbujemy przekazać ISO do funkcji, zgłosi to następujący błąd w czasie wykonywania: TypeError: moment(...).toISO is not a function .

Odnalezienie błędnie napisanego kodu może być trudne. Bieżąca baza kodu może nie mieć ścieżki do przerwanego wiersza, w którym to przypadku nasza uszkodzona referencja .toISO() nie zostałaby przechwycona przez testowanie.

Jeśli przeniesiemy ten kod do TypeScript, IDE podświetli uszkodzone odniesienie, prosząc nas o wprowadzenie poprawek. Jeśli nic nie zrobimy i spróbujemy transpilować, zostaniemy zablokowani, a transpiler wygeneruje następujący błąd:

 TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());

Refaktoryzacja

Chociaż literówki w odwołaniach do kodu stron trzecich nie są rzadkością, istnieje inny zestaw problemów związanych z literówkami w odwołaniach wewnętrznych, jak ten:

 const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); }

Jedyny programista może łatwo zlokalizować i naprawić wszystkie wystąpienia phoneNumbr , aby zakończyć się na er .

Ale im większy zespół, tym bardziej ten prosty, powszechny błąd jest bezpodstawnie kosztowny. W trakcie wykonywania swojej pracy koledzy musieliby być świadomi i propagować takie literówki. Alternatywnie dodanie kodu do obsługi obu pisowni niepotrzebnie rozdęłoby bazę kodu.

W przypadku TypeScript, gdy naprawimy literówkę, kod zależny nie będzie już transpilował, sygnalizując współpracownikom propagację poprawki w ich kodzie.

Dokumentacja

Dokładna i odpowiednia dokumentacja jest kluczem do komunikacji w zespołach programistów i między nimi. Deweloperzy JavaScript często używają JSDoc do dokumentowania oczekiwanych typów metod i właściwości.

Funkcje języka TypeScript (np. klasy abstrakcyjne, interfejsy i definicje typów) ułatwiają programowanie projektowania po kontrakcie, prowadząc do wysokiej jakości dokumentacji. Co więcej, posiadanie formalnej definicji metod i właściwości, których musi przestrzegać obiekt, pomaga identyfikować zmiany, tworzyć testy, przeprowadzać introspekcję kodu i implementować wzorce architektoniczne.

W przypadku TypeScript narzędzie go-to TypeDoc (oparte na propozycji TSDoc) automatycznie wyodrębnia informacje o typie (np. klasie, interfejsie, metodzie i właściwości) z naszego kodu. W ten sposób bez wysiłku tworzymy dokumentację, która jest zdecydowanie bardziej kompleksowa niż dokumentacja JSDoc.

Zalety TypeScriptu w porównaniu z JavaScript

Teraz przyjrzyjmy się, jak możemy użyć TypeScript do rozwiązania tych problemów związanych ze skalowalnością.

Zaawansowane sugestie dotyczące kodu/refaktoryzacji

Wiele środowisk IDE może przetwarzać informacje z systemu typów TypeScript, zapewniając walidację referencji podczas kodowania. Co więcej, gdy piszemy, IDE może dostarczyć odpowiednią, na pierwszy rzut oka dokumentację (np. argumenty, których oczekuje funkcja) dla wszelkich odwołań i zasugerować kontekstowo poprawne nazwy zmiennych.

W tym fragmencie kodu TypeScript IDE sugeruje autouzupełnianie nazw kluczy w wartości zwracanej przez funkcję:

 /** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }

Moje IDE, Visual Studio Code, podało tę sugestię (w objaśnieniu), kiedy zacząłem wywoływać funkcję (wiersz 31):

W momencie wpisywania parsePeopleData() IDE pokazuje podpowiedź z transpilera TypeScript, która brzmi „parsePeopleData(data: string): { people: { name: string; nazwisko: string; age: number; } []; błędy: string[]; }", a następnie tekst zawarty w komentarzu wielowierszowym przed definicją funkcji, "Ciąg zawierający plik CSV z 3 polami: imię, nazwisko, wiek. Prosta funkcja do przetworzenia pliku CSV zawierającego informacje o osobach.".

Co więcej, sugestie autouzupełniania IDE (w objaśnieniu) są poprawne kontekstowo, pokazując tylko prawidłowe nazwy w zagnieżdżonej sytuacji klucza (wiersz 34):

Trzy sugestie (wiek, imię i nazwisko), które pojawiły się w odpowiedzi na wpisanie „map(p => `Nazwa: ${p”. Pierwsza sugestia jest podświetlona i obok niej znajduje się „(właściwość) wiek: liczba”.

Takie sugestie w czasie rzeczywistym prowadzą do szybszego kodowania. Co więcej, IDE mogą polegać na rygorystycznych informacjach o typie TypeScript w celu refaktoryzacji kodu na dowolną skalę. Operacje takie jak zmiana nazwy właściwości, zmiana lokalizacji plików, a nawet wyodrębnienie superklasy stają się trywialne, gdy jesteśmy w 100% pewni dokładności naszych referencji.

Obsługa interfejsu

W przeciwieństwie do JavaScript, TypeScript oferuje możliwość definiowania typów za pomocą interfejsów . Interfejs formalnie zawiera listę — ale nie implementuje — metod i właściwości, które obiekt musi zawierać. Ta konstrukcja języka jest szczególnie pomocna we współpracy z innymi programistami.

Poniższy przykład pokazuje, w jaki sposób możemy wykorzystać funkcje języka TypeScript do porządnego zaimplementowania typowych wzorców OOP — w tym przypadku strategii i łańcucha odpowiedzialności — w ten sposób ulepszając poprzedni przykład:

 export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }

Moduły ES6 — wszędzie

W chwili pisania tego tekstu nie wszystkie środowiska wykonawcze JavaScript typu front-end i back-end obsługują moduły ES6. W przypadku TypeScript możemy jednak użyć składni modułu ES6:

 import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c']));

Przetranspilowane dane wyjściowe będą kompatybilne z naszym wybranym środowiskiem. Na przykład, używając opcji kompilatora --module CommonJS , otrzymujemy:

 "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn;

Używając --module UMD zamiast tego, TypeScript wyświetla bardziej szczegółowy wzorzec UMD:

 (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });

Klasy ES6 — wszędzie

W starszych środowiskach często brakuje wsparcia dla klas ES6. Transpilacja TypeScript zapewnia zgodność przy użyciu konstrukcji specyficznych dla celu. Oto fragment kodu źródłowego TypeScript:

 export class TestClass { hello = 'World'; }

Dane wyjściowe JavaScript zależą zarówno od modułu, jak i celu, co pozwala nam określić TypeScript.

Oto, co --module CommonJS --target es3 :

 "use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass;

Używając --module CommonJS --target es6 zamiast tego otrzymujemy następujący transpilowany wynik. Słowo kluczowe class jest używane do kierowania ES6:

 "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;

Funkcjonalność asynchroniczna/oczekiwania — w dowolnym miejscu

Async/await sprawia, że ​​asynchroniczny kod JavaScript jest łatwiejszy do zrozumienia i utrzymania. TypeScript oferuje tę funkcjonalność we wszystkich środowiskach uruchomieniowych, nawet tych, które nie zapewniają natywnej obsługi asynchronicznej/oczekiwania.

Zauważ, że aby uruchomić async/await w starszych środowiskach wykonawczych, takich jak ES3 i ES5, będziesz potrzebować zewnętrznego wsparcia dla danych wyjściowych opartych na Promise (np. przez Bluebird lub wypełnienie ES2015). Wypełnienie Promise dostarczane z TypeScript łatwo integruje się z transpilowanymi danymi wyjściowymi — wystarczy odpowiednio skonfigurować opcję kompilatora lib .

Wsparcie dla prywatnych pól klasy — w dowolnym miejscu

Nawet w przypadku starszych obiektów docelowych TypeScript obsługuje pola private w taki sam sposób, jak w przypadku języków o silnej typizacji (np. Java lub C#). W przeciwieństwie do tego, wiele środowisk wykonawczych JavaScript obsługuje pola private poprzez składnię prefiksu hash, która jest skończoną propozycją ES2022.

Wady TypeScript vs. JavaScript

Teraz, gdy podkreśliliśmy główne zalety implementacji TypeScript, przyjrzyjmy się scenariuszom, w których TypeScript może nie być odpowiednim rozwiązaniem.

Transpilacja: możliwość wystąpienia niezgodności przepływu pracy

Określone przepływy pracy lub wymagania projektu mogą być niezgodne z etapem transpilacji TypeScript: na przykład, jeśli musieliśmy użyć zewnętrznego narzędzia do zmiany kodu po wdrożeniu lub jeśli wygenerowane dane wyjściowe muszą być przyjazne dla programistów.

Na przykład niedawno napisałem funkcję AWS Lambda dla środowiska Node.js. TypeScript był kiepski, ponieważ wymaganie transpilacji uniemożliwiłoby mi i innym członkom zespołu edytowanie funkcji za pomocą edytora online AWS. To był przełom dla kierownika projektu.

System typu działa tylko do czasu transpilowania

Dane wyjściowe JavaScript TypeScript nie zawierają informacji o typie, więc nie będą sprawdzać typu, a zatem bezpieczeństwo typów może zostać przerwane w czasie wykonywania. Załóżmy na przykład, że funkcja jest zdefiniowana tak, aby zawsze zwracała obiekt. Jeśli null zostanie zwrócona w wyniku użycia w pliku .js , wystąpi błąd w czasie wykonywania.

Funkcje zależne od informacji typu (np. pola prywatne, interfejsy lub typy ogólne) dodają wartość do każdego projektu, ale są usuwane podczas transpilacji. Na przykład członkowie klasy private nie byliby już prywatni po transpilacji. Aby było jasne, tego rodzaju problemy ze środowiskiem wykonawczym nie są charakterystyczne dla TypeScript i można się spodziewać, że napotkasz te same problemy również w przypadku JavaScript.

Łączenie TypeScript i JavaScript

Pomimo wielu zalet TypeScript, czasami nie możemy uzasadnić konwersji całego projektu JavaScript za jednym razem. Na szczęście możemy określić transpilerowi TypeScript — na zasadzie plik po pliku — co należy interpretować jako zwykły JavaScript. W rzeczywistości to hybrydowe podejście może pomóc w łagodzeniu indywidualnych wyzwań pojawiających się w trakcie cyklu życia projektu.

Możemy woleć pozostawić JavaScript bez zmian, jeśli kod:

  • Został napisany przez byłego kolegę i wymagałby znacznych wysiłków w zakresie inżynierii wstecznej, aby przekonwertować go na TypeScript.
  • Używa technik niedozwolonych w TypeScript (np. dodaje właściwość po utworzeniu instancji obiektu) i wymaga refaktoryzacji w celu przestrzegania reguł TypeScript.
  • Należy do innego zespołu, który nadal używa JavaScript.

W takich przypadkach plik deklaracji (plik .d.ts , czasami nazywany plikiem definicji lub plikiem z typami) zapewnia TypeScriptowi wystarczającą ilość danych typu, aby włączyć sugestie IDE, pozostawiając kod JavaScript bez zmian.

Wiele bibliotek JavaScript (np. Lodash, Jest i React) udostępnia pliki do pisania TypeScript w osobnych pakietach typów, podczas gdy inne (np. Moment.js, Axios i Luxon) integrują pliki do pisania z głównym pakietem.

TypeScript vs. JavaScript: kwestia usprawnienia i skalowalności

Bezkonkurencyjne wsparcie, elastyczność i ulepszenia dostępne za pośrednictwem języka TypeScript znacznie poprawiają środowisko programistów, umożliwiając skalowanie projektów i zespołów. Głównym kosztem włączenia TypeScript do projektu jest dodanie kroku kompilacji transpilacji. W przypadku większości aplikacji transpilacja do JavaScript nie stanowi problemu; jest raczej odskocznią do wielu zalet TypeScript.


Dalsza lektura na blogu Toptal Engineering:

  • Praca z obsługą TypeScript i Jest: samouczek AWS SAM