TypeScript: Guida per JavaScript Devs

Introduzione a TypeScript: migliora il tuo JavaScript con la tipizzazione.

Pubblicato:

Introduzione

TypeScript è un superset di JavaScript, il che significa che estende JavaScript aggiungendo la tipizzazione statica e altre funzionalità avanzate. Questo aiuta a migliorare la qualità del codice e a ridurre gli errori. Questa guida ti mostrerà come passare da JavaScript a TypeScript, esplorando i concetti fondamentali e le migliori pratiche.

// Funzione in TypeScript con tipi specificati
function add(a: number, b: number): number {
  return a + b;
}

console.log(add(5, 3)); // Output: 8
console.log(add("5", "3")); // Errore: gli argomenti devono essere numeri

Perché usare TypeScript al posto di JavaScript?

TypeScript offre vari vantaggi rispetto a JavaScript puro:

  • Tipizzazione Statica: Previene errori comuni e rende il codice più sicuro e manutenibile.
  • Auto-completamento Migliorato: Gli editor di codice forniscono suggerimenti migliori grazie alla conoscenza dei tipi.
  • Debugging Semplificato: Gli errori vengono rilevati durante la compilazione, riducendo i bug in fase di esecuzione.
  • Compatibilità: Può essere adottato gradualmente nei progetti esistenti, essendo compatibile con JavaScript.

Tuttavia, ci sono anche alcuni svantaggi:

  • Configurazione e Compilazione: Richiede un processo di compilazione aggiuntivo, aumentando la complessità del build.
  • Curva di Apprendimento: Può essere necessario del tempo per imparare tutte le nuove funzionalità e le best practices.
  • Overhead: La tipizzazione può risultare verbosa, rendendo il codice più lungo e dettagliato.

Per progetti di dimensioni medie o grandi, TypeScript può far risparmiare tempo e ridurre significativamente gli errori. Se hai familiarità con JavaScript, apprendere TypeScript sarà relativamente semplice.

Installazione di TypeScript

Per iniziare a usare TypeScript, installalo con npm. Assicurati di avere Node.js installato, poi esegui:

npm install -g typescript

Verifica l'installazione controllando la versione con il comando:

tsc --version

Compilare TypeScript

Per iniziare a utilizzare TypeScript, crea un file index.ts con il seguente contenuto:

function greet(name: string): string {
  return "Hello, " + name;
}

console.log(greet("World"));

Per compilare il file index.ts in JavaScript, apri il terminale e esegui:

tsc index.ts

Questo genererà un file index.js nella stessa directory, che potrà essere eseguito con Node.js o incluso in un progetto web.

Puoi anche utilizzare l'opzione -w per far sì che TypeScript compili automaticamente i file ogni volta che vengono salvati:

tsc index.ts -w

Questo comando attiverà il watch mode, monitorando il file index.ts per eventuali modifiche e ricompilandolo automaticamente.

Cos'è tsc e la Transpilation

Il comando tsc sta per "TypeScript Compiler". Esso legge il codice TypeScript, verifica la correttezza dei tipi e lo trasforma (o transpila) in JavaScript. La transpilazione è il processo di conversione del codice TypeScript in codice JavaScript, che può essere eseguito su qualsiasi ambiente JavaScript, come browser o Node.js.

È anche possibile configurare il processo di compilazione creando un file tsconfig.json. Questo file consente di specificare diverse opzioni per il compilatore TypeScript. Per creare un file tsconfig.json con le impostazioni di base, puoi eseguire:

tsc --init

Un tipico file tsconfig.json potrebbe apparire così:

{
  "compilerOptions": {
    "target": "es6",  // Versione di JavaScript a cui compilare
    "module": "commonjs", // Sistema di moduli da usare (es. CommonJS per Node.js)
    "strict": true,  // Abilita tutti i controlli di tipo rigorosi
    "esModuleInterop": true,  // Abilita l'interoperabilità dei moduli ES
    "skipLibCheck": true,  // Salta il controllo dei tipi nei file delle librerie
    "forceConsistentCasingInFileNames": true // Enforce consistent casing in file names
  },
  "include": ["src"]  // Specifica quali file includere nella compilazione
}

Dopo aver creato il file tsconfig.json, puoi semplicemente eseguire il comando tsc senza argomenti per compilare tutti i file TypeScript nel progetto secondo le impostazioni specificate:

tsc -w

Questo comando attiverà il watch mode, monitorando tutti i file TypeScript nel progetto per eventuali modifiche e ricompilandoli automaticamente.

Tipi Primitivi in TypeScript

TypeScript fornisce diversi tipi primitivi che puoi utilizzare per definire chiaramente il tipo di dati con cui stai lavorando. I principali tipi primitivi sono:

string: Rappresenta una sequenza di caratteri.

let color: string = "blue";
let fullName: string = `John Doe`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}`;

number: Rappresenta un numero, sia intero che decimale.

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

bigint: Rappresenta un numero intero grande.

let big: bigint = 100n;

boolean: Rappresenta un valore booleano (vero o falso).

let isDone: boolean = false;

undefined: Rappresenta un valore non definito.

let u: undefined = undefined;

null: Rappresenta un valore nullo.

let n: null = null;

symbol: Rappresenta un valore unico e immutabile.

let sym: symbol = Symbol("unique");

Array in TypeScript

In TypeScript, puoi dichiarare array di vari tipi. Ecco alcuni esempi di come dichiarare e usare gli array:

Array di numeri:

let list: number[] = [1, 2, 3];

Array di stringhe:

let colors: string[] = ["red", "green", "blue"];

Array generici: Puoi usare l'oggetto Array con una notazione generica.

let listGeneric: Array<number> = [1, 2, 3];

Oggetti in TypeScript

Gli oggetti in TypeScript sono simili a quelli di JavaScript, ma con l'aggiunta della possibilità di definire tipi per le proprietà. Ecco alcuni esempi di dichiarazione e utilizzo degli oggetti:

Oggetto semplice:

let person: {name: string, age: number} = {
  name: "John",
  age: 30
};

Oggetto con tipi opzionali: Puoi dichiarare proprietà opzionali usando il punto interrogativo (?).

let person: {name: string, age?: number} = {
  name: "John"
};

Oggetto complesso: Puoi nidificare oggetti e array all'interno di altri oggetti.

let company: {
  name: string,
  employees: {name: string, age: number}[],
  isActive: boolean
} = {
  name: "Tech Corp",
  employees: [
    {name: "Alice", age: 30},
    {name: "Bob", age: 25}
  ],
  isActive: true
};

Interfaces in TypeScript

Le interface in TypeScript permettono di definire la struttura degli oggetti in modo più chiaro e riutilizzabile. Ecco come puoi utilizzare le interface per i precedenti esempi di oggetti:

Simple object:

interface Person {
  name: string;
  age: number;
}

let person: Person = {
  name: "John",
  age: 30
};

Optional property: La proprietà age è opzionale grazie al punto interrogativo (?:).

interface Person {
  name: string;
  age?: number;  // age è opzionale
}

let person: Person = {
  name: "John"
};

Complex object:

interface Employee {
  name: string;
  age: number;
}

interface Company {
  name: string;
  employees: Employee[];
  isActive: boolean;
}

let company: Company = {
  name: "Tech Corp",
  employees: [
    {name: "Alice", age: 30},
    {name: "Bob", age: 25}
  ],
  isActive: true
};

Interface con funzione: Puoi anche definire funzioni all'interno delle interface.

interface Person {
  name: string;
  age: number;
  greet: () => string;  // Definizione di una funzione
}

let person: Person = {
  name: "John",
  age: 30,
  greet: function() {
    return `Hello, my name is ${this.name}`;
  }
};

console.log(person.greet()); // Output: Hello, my name is John

Funzioni in TypeScript

Le funzioni in TypeScript possono essere definite con tipi di parametri e tipi di ritorno, migliorando la chiarezza e il controllo sul codice. Ecco alcuni esempi di come dichiarare e utilizzare le funzioni:

Funzione semplice con tipi: Definisce i tipi dei parametri e il tipo di ritorno.

function add(a: number, b: number): number {
  return a + b;
}

console.log(add(5, 3)); // Output: 8

Funzione con parametri opzionali: Il parametro lastName è opzionale grazie al punto interrogativo (?:).

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}

console.log(buildName("John", "Doe")); // Output: John Doe
console.log(buildName("John")); // Output: John

Funzione con parametri di default: Il parametro lastName ha un valore predefinito.

function buildName(firstName: string, lastName: string = "Smith"): string {
  return firstName + " " + lastName;
}

console.log(buildName("John", "Doe")); // Output: John Doe
console.log(buildName("John")); // Output: John Smith

Funzione con rest parameters: Usa l'operatore ... per accettare un numero variabile di parametri.

function buildName(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}

console.log(buildName("John", "Jacob", "Jingleheimer", "Schmidt"));
// Output: John Jacob Jingleheimer Schmidt

Funzioni con interface in TypeScript

Le interface possono essere utilizzate per tipizzare gli oggetti passati come parametri alle funzioni. Ecco alcuni esempi:

Funzione che accetta un oggetto tipizzato:

interface Person {
  name: string;
  age: number;
}

function greet(person: Person): string {
  return `Hello, ${person.name}!`;
}

let user = { name: "John", age: 25 };
console.log(greet(user)); // Output: Hello, John!

Funzione con oggetto e proprietà opzionali:

interface Person {
  name: string;
  age?: number; // age è opzionale
}

function greet(person: Person): string {
  return `Hello, ${person.name}!`;
}

let user1 = { name: "John" };
let user2 = { name: "Mary", age: 30 };
console.log(greet(user1)); // Output: Hello, John!
console.log(greet(user2)); // Output: Hello, Mary!

Funzione con oggetto complesso:

interface Employee {
  name: string;
  age: number;
}

interface Company {
  name: string;
  employees: Employee[];
}

function listEmployees(company: Company): string {
  return company.employees.map(emp => `${emp.name} (${emp.age})`).join(", ");
}

let myCompany: Company = {
  name: "Tech Corp",
  employees: [
    { name: "Alice", age: 30 },
    { name: "Bob", age: 25 }
  ]
};

console.log(listEmployees(myCompany)); // Output: Alice (30), Bob (25)

Tipo void in TypeScript

Il tipo void in TypeScript viene utilizzato per rappresentare l'assenza di un valore, spesso come tipo di ritorno per le funzioni che non restituiscono nulla.

Funzione che non restituisce un valore:

function warnUser(): void {
  console.log("This is a warning message");
}

warnUser();  // Output: This is a warning message

Il tipo void viene spesso utilizzato come tipo di ritorno per le funzioni che eseguono un'azione ma non restituiscono un valore.

Union in TypeScript

Le union in TypeScript permettono di definire una variabile che può contenere più di un tipo. Questo è utile quando una variabile può assumere valori di tipi diversi. Ecco alcuni esempi di come utilizzare le union:

Variabile con tipi uniti:

let id: number | string;
id = 10;  // OK
id = "123";  // OK
// id = true;  // Errore: il tipo 'boolean' non può essere assegnato al tipo 'number | string'

Funzione con parametro di tipo unito:

function printId(id: number | string) {
  console.log(`Il tuo ID è: ${id}`);
}

printId(101);  // Output: Il tuo ID è: 101
printId("202");  // Output: Il tuo ID è: 202

Union con tipi complessi: Puoi combinare oggetti e tipi primitivi.

interface Dog {
  bark: () => void;
}

interface Cat {
  meow: () => void;
}

type Pet = Dog | Cat;

function makeSound(pet: Pet) {
  if ('bark' in pet) {
    pet.bark();
  } else {
    pet.meow();
  }
}

let myDog: Dog = { bark: () => console.log("Woof!") };
let myCat: Cat = { meow: () => console.log("Meow!") };

makeSound(myDog); // Output: Woof!
makeSound(myCat); // Output: Meow!

Literal Types in TypeScript

I literal types in TypeScript permettono di specificare valori esatti che una variabile può assumere. Questo può essere utile per definire tipi più restrittivi e migliorare la sicurezza del codice. Ecco alcuni esempi di come utilizzare i literal types:

String Literal Types:

type Direction = "up" | "down" | "left" | "right";

function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}

move("up");    // OK
move("left");  // OK
// move("forward");  // Errore: il tipo '"forward"' non è assegnabile al tipo 'Direction'

Numeric Literal Types:

type OneToFive = 1 | 2 | 3 | 4 | 5;

function rollDice(value: OneToFive) {
  console.log(`Rolled a ${value}`);
}

rollDice(3);  // OK
// rollDice(6);  // Errore: il tipo '6' non è assegnabile al tipo 'OneToFive'

Boolean Literal Types:

type YesOrNo = "yes" | "no";

function answerQuestion(answer: YesOrNo) {
  if (answer === "yes") {
    console.log("You answered yes.");
  } else {
    console.log("You answered no.");
  }
}

answerQuestion("yes");  // OK
answerQuestion("no");   // OK
// answerQuestion("maybe");  // Errore: il tipo '"maybe"' non è assegnabile al tipo 'YesOrNo'

Combining Literal Types with Union Types:

type Response = "success" | "error";
type StatusCode = 200 | 404 | 500;

function handleResponse(response: Response, code: StatusCode) {
  console.log(`Response: ${response}, Status Code: ${code}`);
}

handleResponse("success", 200);  // OK
handleResponse("error", 404);    // OK
// handleResponse("error", 401);  // Errore: il tipo '401' non è assegnabile al tipo 'StatusCode'

I literal types sono utili per creare tipi più precisi e vincolati, migliorando la sicurezza e la leggibilità del codice. Combinati con i union types, possono definire insiemi di valori possibili in modo chiaro e conciso.

Tipo any in TypeScript

In TypeScript, il tipo any viene utilizzato per rappresentare una variabile che può contenere qualsiasi tipo di dato. Questo tipo è utile quando non si conosce in anticipo il tipo di una variabile o si desidera disabilitare il controllo dei tipi per una determinata variabile. Ecco alcuni esempi di come utilizzare il tipo any:

Variabile di tipo any:

let notSure: any = 4;
notSure = "maybe a string instead";  // OK
notSure = false;  // OK

Funzione con parametro di tipo any:

function logMessage(message: any): void {
  console.log(message);
}

logMessage("Hello, world!");  // Output: Hello, world!
logMessage(42);  // Output: 42
logMessage({ text: "This is a message" });  // Output: { text: "This is a message" }

Array di tipo any: Puoi dichiarare un array che può contenere elementi di qualsiasi tipo.

let mixedArray: any[] = [1, "string", true];
console.log(mixedArray);  // Output: [1, "string", true]

Sebbene il tipo any offra una grande flessibilità, è consigliabile usarlo con cautela. L'uso eccessivo di any può ridurre i benefici del controllo dei tipi di TypeScript, rendendo il codice meno robusto e più difficile da mantenere. In generale, è meglio cercare di usare tipi più specifici possibile.Usare troppo any può essere considerata una bad practice perché annulla molti dei vantaggi che TypeScript offre.

Tipo unknown in TypeScript

Il tipo unknown è simile al tipo any, ma più sicuro. Quando una variabile è di tipo unknown, devi verificare il suo tipo prima di poterla utilizzare. Questo fornisce maggiore sicurezza rispetto all'uso di any. Ecco alcuni esempi di utilizzo del tipo unknown:

Variabile di tipo unknown:

let notSure: unknown = 4;
notSure = "maybe a string instead";  // OK
notSure = false;  // OK

Verifica del tipo prima dell'uso:

function logValue(value: unknown): void {
  if (typeof value === "string") {
    console.log("String: " + value);
  } else if (typeof value === "number") {
    console.log("Number: " + value);
  } else {
    console.log("Unknown type");
  }
}

logValue("Hello, world!");  // Output: String: Hello, world!
logValue(42);  // Output: Number: 42
logValue(true);  // Output: Unknown type

Usare unknown è una buona pratica quando non si è sicuri del tipo di una variabile e si vuole forzare un controllo del tipo prima di utilizzarla. Questo aiuta a prevenire errori e a scrivere codice più sicuro.

Type Assertion (as) in TypeScript

In TypeScript, l'assertione di tipo (type assertion) permette di indicare al compilatore di trattare una variabile come se fosse di un tipo specifico. Questo può essere utile quando si sa più del tipo di una variabile rispetto a ciò che il compilatore può dedurre. L'assertione di tipo non cambia il tipo della variabile a runtime, ma aiuta il compilatore a capire le intenzioni del programmatore.

Uso di as:

let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
console.log(strLength);  // Output: 16

In questo esempio, someValue è dichiarato come unknown. Utilizzando as, affermiamo che someValue è una stringa, permettendoci di accedere alla proprietà length.

Uso alternativo di <>:

let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;
console.log(strLength);  // Output: 16

Questo esempio utilizza la sintassi angolare (<>) per fare un'asserzione di tipo, equivalente a usare as.

Quando usare l'assertione di tipo:

  • Quando si lavora con dati provenienti da fonti esterne e si ha bisogno di indicare al compilatore il tipo previsto.
  • Quando si migra del codice JavaScript esistente a TypeScript e si conoscono i tipi delle variabili.
  • Quando si utilizza una libreria di terze parti e si conosce meglio il tipo di ritorno di una funzione rispetto a ciò che il compilatore può dedurre.

Usa le asserzioni di tipo con cautela, poiché indicano al compilatore di ignorare il controllo dei tipi standard, potenzialmente portando a errori di runtime difficili da diagnosticare.

type in TypeScript

In TypeScript, type permette di creare alias di tipi complessi, rendendo il codice più leggibile e mantenibile. Puoi utilizzare type per definire tipi primitivi, unioni, intersezioni, e persino oggetti complessi. Ecco alcuni esempi di come utilizzare type:

Alias per tipi primitivi:

type ID = number;
type Name = string;

let userId: ID = 123;
let userName: Name = "John Doe";

Alias per unioni:

type Text = string | number;

let text: Text = "Hello";
text = 42;  // OK
// text = true;  // Errore: il tipo 'boolean' non può essere assegnato al tipo 'string | number'

Alias per oggetti complessi:

type Person = {
  name: string;
  age: number;
};

let person: Person = {
  name: "John",
  age: 30
};

Alias per funzioni:

type Greet = (name: string) => string;

let greet: Greet = function(name: string): string {
  return `Hello, ${name}`;
};

console.log(greet("John"));  // Output: Hello, John

Alias per intersezioni:

type Person = {
  name: string;
  age: number;
};

type Employee = Person & {
  role: string;
};

let employee: Employee = {
  name: "John",
  age: 30,
  role: "Developer"
};

L'uso di type in TypeScript permette di definire e riutilizzare facilmente tipi complessi, migliorando la leggibilità e la manutenibilità del codice.

Differenza tra type e interface

In TypeScript, type e interface sono due modi per definire tipi complessi e strutturati, ma hanno alcune differenze nelle loro funzionalità e utilizzi.

Somiglianze tra type e interface

Entrambi possono essere utilizzati per descrivere la forma degli oggetti, inclusi i tipi di proprietà e metodi. Inoltre, gli oggetti possono implementare sia tipi definiti con type sia interfacce definite con interface.

Estensione e Implementazione

Le interface possono estendere altre interfacce e possono essere estese da altre interfacce. Possono anche essere implementate dalle classi.


interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

class Labrador implements Dog {
  name: string;
  breed: string;

  constructor(name: string, breed: string) {
    this.name = name;
    this.breed = breed;
  }
}
        

I type possono combinare tipi usando l'operatore& (intersezione) o | (unione), ma non possono essere estesi nello stesso modo in cui lo fanno le interfacce.


type Animal = {
  name: string;
};

type Dog = Animal & {
  breed: string;
};

const myDog: Dog = {
  name: "Fido",
  breed: "Labrador"
};
        

Dichiarazione Merged

Le interface supportano la "declaration merging", che significa che più dichiarazioni con lo stesso nome si uniscono automaticamente in una singola interfaccia.


interface User {
  name: string;
}

interface User {
  age: number;
}

const user: User = {
  name: "John",
  age: 30
};
        

I type non supportano la "declaration merging". Se si tenta di dichiarare due tipi con lo stesso nome, si ottiene un errore.


type User = {
  name: string;
};

// Error: Duplicate identifier 'User'
type User = {
  age: number;
};
        

Alias di Tipo

I type possono creare alias per tipi primitivi, unioni, tuple e altre costruzioni più complesse.


type StringOrNumber = string | number;

const value: StringOrNumber = "Hello"; // o 42
        

Classi in TypeScript

Le classi in TypeScript sono simili a quelle di JavaScript, ma con il supporto per la tipizzazione statica. Le classi possono avere proprietà, metodi, costruttori e possono implementare interfacce e estendere altre classi. Ecco alcuni esempi di come utilizzare le classi in TypeScript:

Definizione di una classe semplice:

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

let person = new Person("John", 30);
console.log(person.greet());  // Output: Hello, my name is John and I am 30 years old.

Classe con proprietà e metodi privati:

class Person {
  private name: string;
  private age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

let person = new Person("John", 30);
console.log(person.greet());  // Output: Hello, my name is John and I am 30 years old.
// console.log(person.name);  // Errore: la proprietà 'name' è privata.

Classe che implementa un'interfaccia:

interface Greetable {
  greet(): string;
}

class Person implements Greetable {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

let person = new Person("John", 30);
console.log(person.greet());  // Output: Hello, my name is John and I am 30 years old.

Classe che estende un'altra classe:

class Employee extends Person {
  role: string;

  constructor(name: string, age: number, role: string) {
    super(name, age);
    this.role = role;
  }

  greet() {
    return `Hello, my name is ${this.name}, I am ${this.age} years old and I work as a ${this.role}.`;
  }
}

let employee = new Employee("Jane", 28, "Developer");
console.log(employee.greet());  // Output: Hello, my name is Jane, I am 28 years old and I work as a Developer.

Le classi in TypeScript forniscono una sintassi chiara e concisa per creare oggetti e gestire la loro logica. Con l'aggiunta della tipizzazione statica, le classi diventano ancora più potenti e utili per costruire applicazioni robuste e mantenibili.

Enum in TypeScript

Gli enum in TypeScript permettono di definire un insieme di costanti con nomi descrittivi. Gli enum possono essere numerici o stringhe, e sono utili per rappresentare un insieme di valori correlati. Ecco alcuni esempi di come utilizzare gli enum:

Enum numerici:

enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

let dir: Direction = Direction.Up;
console.log(dir);  // Output: 1

In questo esempio, l'enum Direction inizia con il valore 1 per Up. Gli altri membri dell'enum incrementano automaticamente di 1.

Enum di stringhe:

enum Response {
  Yes = "YES",
  No = "NO"
}

function respond(recipient: string, message: Response): void {
  console.log(`${recipient} responded with ${message}`);
}

respond("Alice", Response.Yes);  // Output: Alice responded with YES

In questo esempio, l'enum Response definisce costanti di stringa, utili per rappresentare valori testuali fissi.

Accesso ai membri dell'enum:

enum Status {
  New,
  InProgress,
  Done
}

let currentStatus: Status = Status.InProgress;
console.log(currentStatus);  // Output: 1
console.log(Status[currentStatus]);  // Output: InProgress

È possibile accedere ai membri dell'enum sia tramite il nome che tramite il valore.

Uso degli enum nelle funzioni:

enum Status {
  New,
  InProgress,
  Done
}

function updateStatus(status: Status): void {
  console.log(`Updating status to: ${Status[status]}`);
}

updateStatus(Status.Done);  // Output: Updating status to: Done

Gli enum sono particolarmente utili quando si vuole lavorare con un insieme definito di valori correlati, migliorando la leggibilità e la manutenibilità del codice.

Generics in TypeScript

I generics in TypeScript permettono di creare componenti riutilizzabili che funzionano con vari tipi di dati. Utilizzando i generics, puoi creare funzioni, classi e interfacce che non sono limitate a un singolo tipo di dato. Ecco alcuni esempi di come utilizzare i generics:

Funzione generica:

//In TypeScript, T è una convenzione per indicare un tipo generico,
//mentre K è una convenzione per indicare una chiave di un oggetto generico (K extends keyof T)

function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("Hello, world!");
let output2 = identity<number>(42);

console.log(output1);  // Output: Hello, world!
console.log(output2);  // Output: 42

In questo esempio, la funzione identity utilizza un parametro generico T, che permette di passare e restituire valori di qualsiasi tipo.

Array generico:

function logArrayElements<T>(elements: T[]): void {
  elements.forEach(element => console.log(element));
}

logArrayElements<number>([1, 2, 3]);  // Output: 1, 2, 3
logArrayElements<string>(["a", "b", "c"]);  // Output: a, b, c

Qui, la funzione logArrayElements utilizza un array generico T[], che permette di passare array di qualsiasi tipo.

Classe generica:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;

  constructor(zeroValue: T, addFunction: (x: T, y: T) => T) {
    this.zeroValue = zeroValue;
    this.add = addFunction;
  }
}

let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10));  // Output: 15

In questo esempio, la classe GenericNumber utilizza un parametro generico T per definire le proprietà e i metodi della classe.

Interfaccia generica:

interface Pair<T, U> {
  first: T;
  second: U;
}

let pair: Pair<string, number> = { first: "hello", second: 42 };
console.log(pair);  // Output: { first: "hello", second: 42 }

Qui, l'interfaccia Pair utilizza due parametri generici T e U, permettendo di creare coppie di valori di qualsiasi tipo.

Uso di generics con vincoli:

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
  console.log(arg.length);
}

logLength("hello");  // Output: 5
logLength([1, 2, 3]);  // Output: 3
// logLength(42);  // Errore: il tipo 'number' non ha la proprietà 'length'

In questo esempio, il generico T è vincolato dall'interfaccia Lengthwise, che richiede che il tipo abbia una proprietà length.

I generics sono strumenti potenti che permettono di scrivere codice flessibile e riutilizzabile. Con i generics, puoi creare componenti che funzionano con una varietà di tipi di dati, migliorando la robustezza e la manutenibilità del tuo codice.

Narrowing in TypeScript

In TypeScript, il "narrowing" si riferisce al processo di restringere il tipo di una variabile da un tipo più generico a uno più specifico. Vediamo alcune tecniche principali.

typeof

Usando typeof, possiamo restringere il tipo in base al tipo primitivo.


function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(`String: ${value}`);
  } else {
    console.log(`Number: ${value}`);
  }
}
        

instanceof

Usando instanceof, possiamo restringere il tipo in base alla sua istanza.


class Dog { bark() { console.log("Woof!"); } }
class Cat { meow() { console.log("Meow!"); } }

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}
        

Controllo di Uguaglianza

Possiamo restringere il tipo confrontandolo con un valore specifico.


type Pet = "dog" | "cat";

function getPetSound(pet: Pet) {
  if (pet === "dog") {
    return "Woof!";
  } else {
    return "Meow!";
  }
}
        

Type Guards Personalizzati

Creiamo funzioni personalizzate per restringere il tipo.


function isString(value: any): value is string {
  return typeof value === "string";
}

function printValue(value: string | number) {
  if (isString(value)) {
    console.log(`String: ${value}`);
  } else {
    console.log(`Number: ${value}`);
  }
}
        

Utility Types in TypeScript

TypeScript fornisce diversi utility types predefiniti che permettono di trasformare e manipolare i tipi in modo flessibile. Questi utility types possono semplificare il lavoro con i tipi complessi e aumentare la riusabilità del codice. Ecco una panoramica dei più utili:

Partial<T>

Rende tutte le proprietà di un tipo facoltative.

interface Person {
  name: string;
  age: number;
}

function updatePerson(person: Partial<Person>) {
  // La funzione può accettare un oggetto Person con proprietà parzialmente definite
}

updatePerson({ name: "John" }); // OK
updatePerson({ age: 30 }); // OK
updatePerson({}); // OK

Required<T>

Rende tutte le proprietà di un tipo obbligatorie.

interface Person {
  name?: string;
  age?: number;
}

function printPerson(person: Required<Person>) {
  // La funzione richiede un oggetto Person con tutte le proprietà definite
}

printPerson({ name: "John", age: 30 }); // OK
// printPerson({ name: "John" }); // Errore: manca la proprietà 'age'

Readonly<T>

Rende tutte le proprietà di un tipo di sola lettura.

interface Person {
  name: string;
  age: number;
}

let readonlyPerson: Readonly<Person> = { name: "John", age: 30 };
// readonlyPerson.name = "Doe"; // Errore: 'name' è di sola lettura

Record<K, T>

Crea un tipo oggetto con un insieme di proprietà K di tipo T.

type Role = "admin" | "user" | "guest";

interface Person {
  name: string;
  age: number;
}

const people: Record<Role, Person> = {
  admin: { name: "Alice", age: 25 },
  user: { name: "Bob", age: 30 },
  guest: { name: "Charlie", age: 35 }
};

Pick<T, K>

Crea un tipo prelevando un sottoinsieme delle proprietà da un tipo esistente.

interface Person {
  name: string;
  age: number;
  address: string;
}

type PersonNameAndAge = Pick<Person, "name" | "age">;

const person: PersonNameAndAge = {
  name: "John",
  age: 30
  // address: "123 Street" // Errore: 'address' non è consentito
};

Omit<T, K>

Crea un tipo escludendo un sottoinsieme delle proprietà da un tipo esistente.

interface Person {
  name: string;
  age: number;
  address: string;
}

type PersonWithoutAddress = Omit<Person, "address">;

const person: PersonWithoutAddress = {
  name: "John",
  age: 30
  // address: "123 Street" // Errore: 'address' non è consentito
};

Exclude<T, U>

Rimuove i tipi di U da T.

type AllTypes = string | number | boolean;
type StringOrNumber = Exclude<AllTypes, boolean>;
// StringOrNumber è equivalente a 'string | number'

Extract<T, U>

Estrae i tipi che sono presenti sia in T che in U.

type AllTypes = string | number | boolean;
type StringOrBoolean = Extract<AllTypes, string | boolean>;
// StringOrBoolean è equivalente a 'string | boolean'

NonNullable<T>

Rimuove null e undefined da un tipo.

type NullableTypes = string | number | null | undefined;
type NonNullableTypes = NonNullable<NullableTypes>;
// NonNullableTypes è equivalente a 'string | number'

Questi utility types forniscono potenti strumenti per lavorare con i tipi in TypeScript, migliorando la flessibilità e la manutenibilità del tuo codice.