TypeScript e JavaScript: la tua guida di riferimento

Pubblicato: 2022-03-11

TypeScript o JavaScript? Gli sviluppatori contemplano questa scelta per i progetti web greenfield o Node.js, ma è una domanda che vale la pena considerare anche per i progetti esistenti. Un superset di JavaScript, TypeScript offre tutte le funzionalità di JavaScript più alcuni vantaggi aggiuntivi. TypeScript ci incoraggia intrinsecamente a codificare in modo pulito, rendendo il codice più scalabile. Tuttavia, i progetti possono contenere tutto il JavaScript semplice che desideriamo, quindi l'utilizzo di TypeScript non è una proposta tutto o niente.

La relazione tra TypeScript e JavaScript

TypeScript aggiunge un sistema di tipi esplicito a JavaScript, consentendo l'imposizione rigorosa dei tipi di variabili. TypeScript esegue i suoi controlli di tipo durante la traspilazione , una forma di compilazione che converte il codice TypeScript nel codice JavaScript che i browser Web e Node.js comprendono.

TypeScript vs. Esempi JavaScript

Iniziamo con uno snippet JavaScript valido:

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

Qui, var1 inizia come una string , quindi diventa un number .

Poiché JavaScript è solo vagamente digitato, possiamo ridefinire var1 come una variabile di qualsiasi tipo, da una stringa a una funzione, in qualsiasi momento.

L'esecuzione di questo codice restituisce 10 .

Ora, cambiamo questo codice in TypeScript:

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

In questo caso, dichiariamo var1 come una string . Quindi proviamo ad assegnargli un numero, che non è consentito dal sistema di tipi rigorosi di TypeScript. La traslazione genera un errore:

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

Se dovessimo indicare al transpiler di trattare lo snippet JavaScript originale come se fosse TypeScript, il transpiler dedurrebbe automaticamente che var1 dovrebbe essere una string | number string | number . Questo è un tipo di unione TypeScript , che ci consente di assegnare a var1 una string o un number in qualsiasi momento. Dopo aver risolto il conflitto di tipo, il nostro codice TypeScript sarebbe stato transpilato correttamente. L'esecuzione produrrebbe lo stesso risultato dell'esempio JavaScript.

TypeScript e JavaScript da 30.000 piedi: sfide di scalabilità

JavaScript è onnipresente, alimenta progetti di tutte le dimensioni, applicato in modi che sarebbero stati inimmaginabili durante la sua infanzia negli anni '90. Sebbene JavaScript sia maturato, non è all'altezza del supporto della scalabilità. Di conseguenza, gli sviluppatori sono alle prese con applicazioni JavaScript che sono cresciute sia in termini di grandezza che di complessità.

Per fortuna, TypeScript risolve molti dei problemi di ridimensionamento dei progetti JavaScript. Ci concentreremo sulle tre sfide principali: convalida, refactoring e documentazione.

Convalida

Facciamo affidamento sugli ambienti di sviluppo integrati (IDE) per aiutare con attività come l'aggiunta, la modifica e il test di nuovo codice, ma gli IDE non possono convalidare riferimenti JavaScript puri. Riduciamo questa lacuna monitorando attentamente mentre codifichiamo per evitare la possibilità di errori di battitura nelle variabili e nei nomi delle funzioni.

L'entità del problema aumenta in modo esponenziale quando il codice proviene da una terza parte, dove i riferimenti interrotti in rami di codice eseguiti di rado potrebbero facilmente passare inosservati.

Al contrario, con TypeScript, possiamo concentrare i nostri sforzi sulla codifica, certi che eventuali errori verranno identificati al momento del transpile. Per dimostrarlo, iniziamo con del codice JavaScript legacy:

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

La chiamata .toISO() è un errore di battitura del metodo moment.js toISOString() ma il codice funzionerebbe, a condizione che l'argomento format non sia ISO . La prima volta che proviamo a passare ISO alla funzione, verrà generato questo errore di runtime: TypeError: moment(...).toISO is not a function .

L'individuazione del codice errato può essere difficile. La base di codice corrente potrebbe non avere un percorso verso la linea spezzata, nel qual caso il nostro riferimento .toISO() interrotto non verrebbe rilevato dai test.

Se portiamo questo codice su TypeScript, l'IDE evidenzierebbe il riferimento interrotto, chiedendoci di apportare correzioni. Se non facciamo nulla e tentiamo di transpilare, verremmo bloccati e il transpiler genererebbe il seguente errore:

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

Refactoring

Sebbene gli errori di battitura nei riferimenti di codice di terze parti non siano rari, esiste un diverso insieme di problemi associati agli errori di battitura nei riferimenti interni, come questo:

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

Un unico sviluppatore può individuare e riparare tutte le istanze di phoneNumbr per terminare con er abbastanza facilmente.

Ma più grande è la squadra, più questo semplice errore comune è irragionevolmente costoso. Nel corso dello svolgimento del proprio lavoro, i colleghi dovrebbero essere consapevoli e propagare tali errori di battitura. In alternativa, l'aggiunta di codice per supportare entrambe le ortografie rigonfierebbe inutilmente la base di codice.

Con TypeScript, quando correggiamo un errore di battitura, il codice dipendente non verrà più trascritto, segnalando ai colleghi di propagare la correzione al loro codice.

Documentazione

Una documentazione accurata e pertinente è fondamentale per la comunicazione all'interno e tra i team di sviluppatori. Gli sviluppatori JavaScript utilizzano spesso JSDoc per documentare i metodi e i tipi di proprietà previsti.

Le caratteristiche del linguaggio di TypeScript (ad es. classi astratte, interfacce e definizioni di tipo) facilitano la programmazione design-by-contract, portando a una documentazione di qualità. Inoltre, avere una definizione formale dei metodi e delle proprietà a cui un oggetto deve aderire aiuta a identificare le modifiche sostanziali, creare test, eseguire l'introspezione del codice e implementare modelli architetturali.

Per TypeScript, lo strumento di riferimento TypeDoc (basato sulla proposta TSDoc) estrae automaticamente le informazioni sul tipo (ad esempio, classe, interfaccia, metodo e proprietà) dal nostro codice. Pertanto, creiamo facilmente una documentazione che è di gran lunga più completa di quella di JSDoc.

Vantaggi di TypeScript rispetto a JavaScript

Ora, esploriamo come possiamo usare TypeScript per affrontare queste sfide di scalabilità.

Suggerimenti avanzati di codice/refactoring

Molti IDE possono elaborare le informazioni dal sistema di tipi TypeScript, fornendo la convalida dei riferimenti durante la codifica. Ancora meglio, mentre digitiamo, l'IDE può fornire documentazione pertinente e immediata (ad esempio, gli argomenti che una funzione si aspetta) per qualsiasi riferimento e suggerire nomi di variabili contestualmente corretti.

In questo frammento di codice TypeScript, l'IDE suggerisce un completamento automatico dei nomi delle chiavi all'interno del valore restituito della funzione:

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

Il mio IDE, Visual Studio Code, ha fornito questo suggerimento (nel callout) quando ho iniziato a chiamare la funzione (riga 31):

Al momento della digitazione di parsePeopleData(), l'IDE mostra una descrizione comando dal transpiler TypeScript che legge "parsePeopleData(data: string): { people: { name: string; surname: string; age: number; }[]; errors: string[]; }" seguito dal testo contenuto nel commento multilinea prima della definizione della funzione, "Una stringa contenente un CSV con 3 campi: nome, cognome, età. Semplice funzione per analizzare un CSV contenente informazioni sulle persone.".

Inoltre, i suggerimenti di completamento automatico dell'IDE (nel callout) sono contestualmente corretti, mostrando solo nomi validi all'interno di una situazione di chiave nidificata (riga 34):

Tre suggerimenti (età, nome e cognome) che sono comparsi in risposta alla digitazione di "map(p => `Nome: ${p." Il primo suggerimento è evidenziato e ha accanto "(proprietà) età: numero".

Tali suggerimenti in tempo reale portano a una codifica più rapida. Inoltre, gli IDE possono fare affidamento sulle rigorose informazioni sul tipo di TypeScript per il refactoring del codice su qualsiasi scala. Operazioni come la ridenominazione di una proprietà, la modifica delle posizioni dei file o persino l'estrazione di una superclasse diventano banali quando siamo sicuri al 100% dell'accuratezza dei nostri riferimenti.

Supporto interfaccia

A differenza di JavaScript, TypeScript offre la possibilità di definire i tipi utilizzando le interfacce . Un'interfaccia elenca formalmente, ma non implementa, i metodi e le proprietà che un oggetto deve includere. Questo costrutto di linguaggio è particolarmente utile per la collaborazione con altri sviluppatori.

L'esempio seguente evidenzia come possiamo sfruttare le funzionalità di TypeScript per implementare in modo ordinato modelli OOP comuni, in questo caso, strategia e catena di responsabilità , migliorando così l'esempio precedente:

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

Moduli ES6: ovunque

Al momento della stesura di questo documento, non tutti i runtime JavaScript front-end e back-end supportano i moduli ES6. Con TypeScript, invece, possiamo usare la sintassi del modulo ES6:

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

L'output trasferito sarà compatibile con il nostro ambiente selezionato. Ad esempio, usando l'opzione del compilatore --module CommonJS , otteniamo:

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

Usando invece --module UMD , TypeScript restituisce il modello UMD più dettagliato:

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

Classi ES6: ovunque

Gli ambienti legacy spesso non supportano le classi ES6. Una transpile TypeScript garantisce la compatibilità utilizzando costrutti specifici della destinazione. Ecco uno snippet sorgente TypeScript:

 export class TestClass { hello = 'World'; }

L'output di JavaScript dipende sia dal modulo che dalla destinazione, che TypeScript ci consente di specificare.

Ecco cosa --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;

Usando --module CommonJS --target es6 , otteniamo il seguente risultato transpilato. La parola chiave class viene utilizzata per scegliere come target ES6:

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

Funzionalità Async/Await: ovunque

Async/await semplifica la comprensione e la gestione del codice JavaScript asincrono. TypeScript offre questa funzionalità a tutti i runtime, anche a quelli che non forniscono async/await in modo nativo.

Nota che per eseguire async/await su runtime precedenti come ES3 ed ES5, avrai bisogno del supporto esterno per l'output basato su Promise (ad esempio, tramite Bluebird o un polyfill ES2015). Il polyfill Promise fornito con TypeScript si integra facilmente nell'output transpilato: dobbiamo solo configurare l'opzione del compilatore lib di conseguenza.

Supporto per i campi delle classi private: ovunque

Anche per le destinazioni legacy, TypeScript supporta i campi private ​​più o meno allo stesso modo dei linguaggi fortemente tipizzati (ad esempio, Java o C#). Al contrario, molti runtime JavaScript supportano i campi private ​​tramite la sintassi del prefisso hash, che è una proposta finita di ES2022.

Svantaggi di TypeScript rispetto a JavaScript

Ora che abbiamo evidenziato i principali vantaggi dell'implementazione di TypeScript, esploriamo gli scenari in cui TypeScript potrebbe non essere adatto.

Traspirazione: potenziale incompatibilità del flusso di lavoro

Flussi di lavoro o requisiti di progetto specifici potrebbero essere incompatibili con la fase di trascrizione di TypeScript: ad esempio, se abbiamo bisogno di utilizzare uno strumento esterno per modificare il codice dopo la distribuzione o se l'output generato deve essere di facile utilizzo per gli sviluppatori.

Ad esempio, di recente ho scritto una funzione AWS Lambda per un ambiente Node.js. TypeScript non era adatto perché richiedere la traspirazione avrebbe impedito a me e ad altri membri del team di modificare la funzione utilizzando l'editor online di AWS. Questo è stato un problema per il project manager.

Tipo Il sistema funziona solo fino al momento del transito

L'output JavaScript di TypeScript non contiene informazioni sul tipo, quindi non eseguirà controlli del tipo e, pertanto, la sicurezza del tipo può interrompersi in fase di esecuzione. Ad esempio, supponiamo che una funzione sia definita per restituire sempre un oggetto. Se null viene restituito dal suo utilizzo all'interno di un file .js , si verificherà un errore di runtime.

Digitare le funzionalità dipendenti dalle informazioni (ad es. campi privati, interfacce o generici) aggiungono valore a qualsiasi progetto ma vengono eliminate durante il transpiling. Ad esempio, i membri private ​​della classe non sarebbero più privati ​​dopo la traspirazione. Per essere chiari, problemi di runtime di questa natura non sono esclusivi di TypeScript e puoi aspettarti di incontrare le stesse difficoltà anche con JavaScript.

Combinare TypeScript e JavaScript

Nonostante i numerosi vantaggi di TypeScript, a volte non possiamo giustificare la conversione di un intero progetto JavaScript tutto in una volta. Fortunatamente, possiamo specificare al transpiler TypeScript, file per file, cosa interpretare come JavaScript semplice. In effetti, questo approccio ibrido può aiutare a mitigare le sfide individuali che si presentano nel corso del ciclo di vita di un progetto.

Potremmo preferire lasciare JavaScript invariato se il codice:

  • È stato scritto da un ex collega e richiederebbe notevoli sforzi di reverse engineering per la conversione in TypeScript.
  • Utilizza le tecniche non consentite in TypeScript (ad esempio, aggiunge una proprietà dopo l'istanza di un oggetto) e richiederebbe il refactoring per aderire alle regole di TypeScript.
  • Appartiene a un altro team che continua a utilizzare JavaScript.

In questi casi, un file di dichiarazione (file .d.ts , a volte chiamato file di definizione o file di tipizzazione) fornisce a TypeScript dati di tipo sufficienti per abilitare i suggerimenti IDE lasciando il codice JavaScript così com'è.

Molte librerie JavaScript (es. Lodash, Jest e React) forniscono file di tipizzazione TypeScript in pacchetti di tipi separati, mentre altre (es. Moment.js, Axios e Luxon) integrano file di tipizzazione nel pacchetto principale.

TypeScript vs JavaScript: una questione di razionalizzazione e scalabilità

Il supporto, la flessibilità e i miglioramenti senza rivali disponibili tramite TypeScript migliorano notevolmente l'esperienza degli sviluppatori, consentendo la scalabilità di progetti e team. Il costo principale dell'incorporazione di TypeScript in un progetto è l'aggiunta della fase di costruzione della traspirazione. Per la maggior parte delle applicazioni, il transpiling in JavaScript non è un problema; piuttosto, è un trampolino di lancio per i numerosi vantaggi di TypeScript.


Ulteriori letture sul blog di Toptal Engineering:

  • Lavorare con TypeScript e Jest Support: un tutorial AWS SAM