TypeScript vs. JavaScript: Ghidul dvs. de acces

Publicat: 2022-03-11

TypeScript sau JavaScript? Dezvoltatorii iau în considerare această alegere pentru proiectele web greenfield sau Node.js, dar este o întrebare care merită luată în considerare și pentru proiectele existente. Un superset de JavaScript, TypeScript oferă toate caracteristicile JavaScript plus câteva avantaje suplimentare. TypeScript ne încurajează în mod intrinsec să codificăm curat, făcând codul mai scalabil. Cu toate acestea, proiectele pot conține atât JavaScript simplu cât ne place, așa că folosirea TypeScript nu este o propunere de tot sau nimic.

Relația dintre TypeScript și JavaScript

TypeScript adaugă un sistem de tip explicit la JavaScript, permițând aplicarea strictă a tipurilor de variabile. TypeScript își execută verificările de tip în timp ce transpilează — o formă de compilare care convertește codul TypeScript în codul JavaScript pe care browserele web și Node.js le înțeleg.

Exemple TypeScript vs. JavaScript

Să începem cu un fragment JavaScript valid:

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

Aici, var1 începe ca un string , apoi devine un number .

Întrucât JavaScript este scris doar vag, putem redefini var1 ca o variabilă de orice tip - de la un șir la o funcție - în orice moment.

Executarea acestui cod iese 10 .

Acum, să schimbăm acest cod în TypeScript:

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

În acest caz, declarăm var1 ca fiind un string . Apoi încercăm să îi atribuim un număr, ceea ce nu este permis de sistemul strict de tipare al TypeScript. Transpilarea are ca rezultat o eroare:

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

Dacă ar fi să instruim transpilerul să trateze fragmentul JavaScript original ca și cum ar fi TypeScript, transpilerul ar deduce automat că var1 ar trebui să fie un string | number string | number . Acesta este un tip de unire TypeScript, care ne permite să atribuim var1 un string sau un number în orice moment. După ce s-a rezolvat conflictul de tip, codul nostru TypeScript va fi transpilat cu succes. Executarea acestuia ar produce același rezultat ca exemplul JavaScript.

TypeScript vs. JavaScript de la 30.000 de picioare: provocări de scalabilitate

JavaScript este omniprezent, susținând proiecte de toate dimensiunile, aplicat în moduri care ar fi fost de neimaginat în perioada incipientă, în anii 1990. Deși JavaScript s-a maturizat, este insuficient atunci când vine vorba de suport pentru scalabilitate. În consecință, dezvoltatorii se confruntă cu aplicațiile JavaScript care au crescut atât în ​​amploare, cât și în complexitate.

Din fericire, TypeScript abordează multe dintre problemele de scalare a proiectelor JavaScript. Ne vom concentra pe primele trei provocări: validare, refactorizare și documentare.

Validare

Ne bazăm pe medii de dezvoltare integrate (IDE-uri) pentru a ajuta cu sarcini precum adăugarea, modificarea și testarea codului nou, dar IDE-urile nu pot valida referințe JavaScript pure. Atenuăm acest neajuns prin monitorizarea vigilentă în timp ce codificăm pentru a evita posibilitatea de greșeli de scriere în variabile și numele funcțiilor.

Amploarea problemei crește exponențial atunci când codul provine de la o terță parte, unde referințele sparte în ramurile de cod executate rar ar putea rămâne cu ușurință nedetectate.

În contrast, cu TypeScript, ne putem concentra eforturile pe codificare, încrezători că orice erori va fi identificată în momentul transpilării. Pentru a demonstra acest lucru, să începem cu un cod JavaScript vechi:

 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() este o greșeală de tipar a momentului.js metoda toISOString() dar codul ar funcționa, cu condiția ca argumentul format să nu fie ISO . Prima dată când încercăm să transmitem ISO funcției, aceasta va genera această eroare de rulare: TypeError: moment(...).toISO is not a function .

Localizarea codului greșit poate fi dificilă. Este posibil ca baza de cod curentă să nu aibă o cale către linia întreruptă, caz în care referința noastră .toISO() întreruptă nu va fi prinsă de testare.

Dacă portăm acest cod la TypeScript, IDE-ul ar evidenția referința ruptă, solicitându-ne să facem corecții. Dacă nu facem nimic și încercăm să transpilăm, am fi blocați, iar transpiler-ul va genera următoarea eroare:

 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());

Refactorizarea

Deși greșelile de scriere în referințele de coduri terță parte nu sunt neobișnuite, există un set diferit de probleme asociate greșelilor de scriere în referințele interne, cum ar fi acesta:

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

Un singur dezvoltator poate localiza și remedia toate cazurile de phoneNumbr pentru a se termina cu er suficient de ușor.

Dar cu cât echipa este mai mare, cu atât această greșeală simplă și comună este nerezonabil de costisitoare. În timpul îndeplinirii activității lor, colegii ar trebui să cunoască și să propage astfel de greșeli de scriere. Alternativ, adăugarea de cod pentru a accepta ambele ortografii ar umfla inutil baza de cod.

Cu TypeScript, atunci când remediam o greșeală de tipar, codul dependent nu se va mai transpila, semnalându-le colegilor să propage remedierea în codul lor.

Documentație

Documentația corectă și relevantă este cheia comunicării în cadrul și între echipele de dezvoltatori. Dezvoltatorii JavaScript folosesc adesea JSDoc pentru a documenta metodele așteptate și tipurile de proprietăți.

Caracteristicile limbajului TypeScript (de exemplu, clase abstracte, interfețe și definiții de tip) facilitează programarea design-by-contract, ducând la o documentare de calitate. Mai mult decât atât, a avea o definiție formală a metodelor și proprietăților la care un obiect trebuie să adere, ajută la identificarea schimbărilor de ruptură, crearea de teste, efectuarea introspecției codului și implementarea modelelor arhitecturale.

Pentru TypeScript, instrumentul de acces TypeDoc (bazat pe propunerea TSDoc) extrage automat informații de tip (de exemplu, clasă, interfață, metodă și proprietate) din codul nostru. Astfel, creăm fără efort o documentație care este, de departe, mai cuprinzătoare decât cea a JSDoc.

Avantajele TypeScript vs. JavaScript

Acum, să explorăm cum putem folosi TypeScript pentru a aborda aceste provocări de scalabilitate.

Cod avansat/Sugestii de refactorizare

Multe IDE-uri pot procesa informații din sistemul de tip TypeScript, oferind validarea referințelor pe măsură ce codificăm. Și mai bine, pe măsură ce scriem, IDE-ul poate furniza documentație relevantă, dintr-o privire (de exemplu, argumentele pe care le așteaptă o funcție) pentru orice referință și poate sugera nume de variabile corecte din punct de vedere contextual.

În acest fragment TypeScript, IDE-ul sugerează o completare automată a numelor cheilor din interiorul valorii returnate a funcției:

 /** * 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')); }

IDE-ul meu, Visual Studio Code, a oferit această sugestie (în înștiințare) când am început să apelez funcția (linia 31):

În momentul introducerii parsePeopleData(), IDE-ul afișează un tooltip din transpilerul TypeScript care citește „parsePeopleData(data: string): { people: { name: string; surname: string; age: number; }[]; erori: șir[]; }" urmat de textul conținut în comentariul pe mai multe rânduri înainte de definiția funcției, "Un șir care conține un CSV cu 3 câmpuri: nume, prenume, vârstă. Funcție simplă pentru a analiza un CSV care conține informații despre persoane.".

În plus, sugestiile de completare automată ale IDE (în înștiințare) sunt corecte din punct de vedere contextual, afișând numai nume valide într-o situație cheie imbricată (linia 34):

Trei sugestii (vârstă, nume și prenume) care au apărut ca răspuns la tastarea „map(p => `Nume: ${p.” Prima sugestie este evidențiată și are „(proprietate) vârstă: număr” lângă ea.

Astfel de sugestii în timp real duc la o codificare mai rapidă. Mai mult, IDE-urile se pot baza pe informațiile riguroase de tip ale TypeScript pentru a refactoriza codul la orice scară. Operațiuni precum redenumirea unei proprietăți, schimbarea locațiilor fișierelor sau chiar extragerea unei superclase devin banale atunci când suntem 100% încrezători în acuratețea referințelor noastre.

Suport pentru interfață

Spre deosebire de JavaScript, TypeScript oferă posibilitatea de a defini tipuri folosind interfețe . O interfață listează în mod oficial, dar nu implementează, metodele și proprietățile pe care un obiect trebuie să le includă. Această construcție a limbajului este deosebit de utilă pentru colaborarea cu alți dezvoltatori.

Următorul exemplu evidențiază modul în care putem folosi funcțiile TypeScript pentru a implementa modele OOP comune – în acest caz, strategia și lanțul de responsabilitate – îmbunătățind astfel exemplul anterior:

 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')); }

Module ES6 — Oriunde

În momentul scrierii acestui articol, nu toate runtimele JavaScript front-end și back-end acceptă modulele ES6. Cu TypeScript, totuși, putem folosi sintaxa modulului ES6:

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

Ieșirea transpilată va fi compatibilă cu mediul nostru selectat. De exemplu, folosind opțiunea de compilare --module CommonJS , obținem:

 "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;

Folosind în schimb --module UMD , TypeScript produce modelul UMD mai pronunțat:

 (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; });

Clasele ES6 — Oriunde

Mediile vechi nu suportă adesea clasele ES6. Un transpile TypeScript asigură compatibilitatea utilizând constructe specifice țintei. Iată un fragment sursă TypeScript:

 export class TestClass { hello = 'World'; }

Ieșirea JavaScript depinde atât de modul, cât și de țintă, pe care TypeScript ne permite să-l specificăm.

Iată ce --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;

Folosind --module CommonJS --target es6 , obținem următorul rezultat transpilat. Cuvântul cheie class este folosit pentru a viza ES6:

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

Funcționalitate Async/Await — Oriunde

Async/wait face codul JavaScript asincron mai ușor de înțeles și de întreținut. TypeScript oferă această funcționalitate tuturor timpilor de execuție, chiar și celor care nu oferă asincron/așteptare nativ.

Rețineți că pentru a rula async/wait pe runtime mai vechi, cum ar fi ES3 și ES5, veți avea nevoie de suport extern pentru ieșirea bazată pe Promise (de exemplu, prin Bluebird sau un polyfill ES2015). Polyfill-ul Promise care este livrat cu TypeScript se integrează cu ușurință în ieșirea transpilată - trebuie doar să configuram opțiunea compilatorului lib în consecință.

Suport pentru câmpurile de clasă privată — Oriunde

Chiar și pentru ținte vechi, TypeScript acceptă câmpuri private în același mod ca și limbajele puternic tipizate (de exemplu, Java sau C#). În schimb, multe runtime JavaScript acceptă câmpuri private prin sintaxa prefixului hash, care este o propunere finală a ES2022.

Dezavantajele TypeScript vs. JavaScript

Acum că am evidențiat principalele beneficii ale implementării TypeScript, haideți să explorăm scenarii în care TypeScript ar putea să nu fie potrivit.

Transpilare: potențial de incompatibilitate cu fluxul de lucru

Fluxurile de lucru specifice sau cerințele de proiect pot fi incompatibile cu pasul de transpilare al TypeScript: de exemplu, dacă trebuie să folosim un instrument extern pentru a schimba codul după implementare sau dacă rezultatul generat trebuie să fie ușor de dezvoltat.

De exemplu, am scris recent o funcție AWS Lambda pentru un mediu Node.js. TypeScript a fost o potrivire slabă, deoarece necesitatea transpilării ne-ar împiedica pe mine și pe alți membri ai echipei să edităm funcția folosind editorul online AWS. Acesta a fost un detaliu pentru managerul de proiect.

Tip Sistemul funcționează numai până la momentul transpilării

Ieșirea JavaScript a TypeScript nu conține informații de tip, așa că nu va efectua verificări de tip și, prin urmare, siguranța tipului se poate întrerupe în timpul execuției. De exemplu, să presupunem că o funcție este definită să returneze întotdeauna un obiect. Dacă null este returnat din utilizarea sa într-un fișier .js , va apărea o eroare de rulare.

Caracteristicile dependente de informații de tip (de exemplu, câmpuri private, interfețe sau generice) adaugă valoare oricărui proiect, dar sunt eliminate în timpul transpilării. De exemplu, membrii clasei private nu ar mai fi private după transpilare. Pentru a fi clar, problemele de rulare de această natură nu sunt unice pentru TypeScript și vă puteți aștepta să întâmpinați aceleași dificultăți și cu JavaScript.

Combinând TypeScript și JavaScript

În ciuda numeroaselor beneficii ale TypeScript, uneori nu putem justifica conversia unui întreg proiect JavaScript dintr-o dată. Din fericire, putem specifica transpilerului TypeScript - pe bază de fișier cu fișier - ce să interpretăm ca JavaScript simplu. De fapt, această abordare hibridă poate ajuta la atenuarea provocărilor individuale pe măsură ce acestea apar pe parcursul ciclului de viață al unui proiect.

Este posibil să preferăm să lăsăm JavaScript neschimbat dacă codul:

  • A fost scris de un fost coleg și ar necesita eforturi semnificative de inginerie inversă pentru a converti la TypeScript.
  • Folosește tehnici nepermise în TypeScript (de exemplu, adaugă o proprietate după instanțierea obiectului) și ar necesita refactorizare pentru a adera la regulile TypeScript.
  • Aparține unei alte echipe care continuă să folosească JavaScript.

În astfel de cazuri, un fișier de declarație (fișier .d.ts , numit uneori fișier de definiție sau fișier de tastare) oferă TypeScript suficiente date de tip pentru a activa sugestiile IDE, lăsând codul JavaScript așa cum este.

Multe biblioteci JavaScript (de exemplu, Lodash, Jest și React) oferă fișiere de tastare TypeScript în pachete de tip separate, în timp ce altele (de exemplu, Moment.js, Axios și Luxon) integrează fișiere de tastare în pachetul principal.

TypeScript vs. JavaScript: O chestiune de simplificare și scalabilitate

Suportul de neegalat, flexibilitatea și îmbunătățirile disponibile prin TypeScript îmbunătățesc semnificativ experiența dezvoltatorului, permițând proiectelor și echipelor să se extindă. Costul principal al încorporării TypeScript într-un proiect este adăugarea etapei de construire a transpilării. Pentru majoritatea aplicațiilor, transpilarea în JavaScript nu este o problemă; mai degrabă, este o piatră de temelie către numeroasele beneficii ale TypeScript.


Citiți suplimentare pe blogul Toptal Engineering:

  • Lucrul cu TypeScript și suport Jest: un tutorial AWS SAM