28 aprile 2025

Padroneggiare le funzionalità avanzate di TypeScript per applicazioni complesse

TypeScript è un linguaggio estremamente potente che va ben oltre le semplici annotazioni di tipo. Con l’aumentare della complessità delle tue applicazioni, le funzionalità avanzate di TypeScript ti forniscono gli strumenti necessari per scrivere codice più sofisticato, flessibile e affidabile.

In questo articolo esploreremo alcune delle funzionalità avanzate di TypeScript che ti aiuteranno a gestire scenari complessi. Vedremo i Type Guard, i Mapped Type, i Conditional Type, gli Utility Type e le Discriminated Union.

Type Guard: restringere i tipi con sicurezza

In TypeScript, un type guard è una tecnica utilizzata per restringere il tipo di una variabile. I type guard permettono di scrivere logica più specifica basata sul tipo reale di un valore.

Utilizzo dei Type Guard incorporati

Operatore typeof

Puoi usare l’operatore typeof per controllare i tipi primitivi:

function printLength(value: string | number) {
  if (typeof value === "string") {
    console.log(value.length); // TypeScript knows `value` is a string here
  } else {
    console.log(value.toFixed(2)); // `value` is a number here
  }
}

In questo esempio, TypeScript restringe automaticamente il tipo di value a string o number in base al controllo effettuato con typeof.

Operatore instanceof

L’operatore instanceof verifica se un oggetto è un’istanza di una classe specifica:

class Animal {
  move() {}
}

class Bird extends Animal {
  fly() {}
}

function makeSound(animal: Animal) {
  if (animal instanceof Bird) {
    animal.fly(); // TypeScript knows `animal` is a `Bird` here
  }
}

Qui, instanceof permette a TypeScript di riconoscere esattamente il tipo di animal quando è un Bird, fornendo tipi più precisi per metodi come fly().

Type Guard personalizzati

Puoi creare type guard personalizzati per affinare ulteriormente la logica:

interface Dog {
  type: "dog";
  bark(): void;
}

interface Cat {
  type: "cat";
  meow(): void;
}

function isDog(animal: Dog | Cat): animal is Dog {
  return animal.type === "dog";
}

const pet: Dog | Cat = { type: "dog", bark: () => {} };

if (isDog(pet)) {
  pet.bark(); // TypeScript knows `pet` is a `Dog` here
}

La funzione isDog è una type guard personalizzata che restringe il tipo di animal a Dog se il suo tipo è “dog”.

Mapped Type: trasformare le proprietà dei tipi

I Mapped Type ti permettono di creare un nuovo tipo trasformando le proprietà di un tipo esistente.

Mapped Type di base

Ecco come creare un tipo che rende tutte le proprietà opzionali:

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

type PartialUser = { [K in keyof User]?: User[K] }; // All properties are optional

Mapped Type più complessi

Puoi anche trasformare le proprietà in readonly:

type ReadOnlyUser = { readonly [K in keyof User]: User[K] }; // Makes all properties read-only

Questo crea un tipo ReadOnlyUser, in cui ogni proprietà è contrassegnata come readonly (sola lettura).

Tipi Condizionali: Tipi Dipendenti da Condizioni

I Tipi Condizionali ti permettono di definire tipi basati su determinate condizioni. Questi tipi vengono spesso utilizzati per creare codice più dinamico e flessibile.

Tipi Condizionali di Base

Un tipo condizionale assume la forma T extends U ? X : Y. Se il tipo T estende U, il risultato sarà X; altrimenti sarà Y.

type IsString<T> = T extends string ? "Yes" : "No";

type Test1 = IsString<string>; // "Yes"
type Test2 = IsString<number>; // "No"

Tipi Condizionali con infer

Puoi utilizzare la parola chiave infer per dedurre un tipo all’interno di un controllo condizionale:

type ReturnType<T> = T extends (args: any[]) => infer R ? R : never;

type FunctionReturnType = ReturnType<() => string>; // string

Qui, infer R estrae il tipo di ritorno di una funzione, e ReturnType viene definito in base al fatto che il tipo di input sia o meno una funzione.

Tipi di Utilità: Modificatori di Tipo Integrati

TypeScript offre diversi tipi di utilità integrati per rendere il lavoro con i tipi più semplice e flessibile.

Partial<T>

Partial<T> rende tutte le proprietà di T opzionali:

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

const partialUser: Partial<User> = {}; // Both `name` and `age` are optional

Required<T>

Required<T> rende tutte le proprietà di T obbligatorie:

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

const fullUser: Required<User> = { name: "Alice", age: 30 }; // Both `name` and `age` are now required

Readonly<T>

Readonly<T> rende tutte le proprietà di T read-only:

const user: Readonly<User> = { name: "Alice", age: 30 };
// user.age = 31;  // Error: Cannot assign to 'age' because it is a read-only property

Altri tipi di utilità

Questi tipi di utilità semplificano il lavoro con tipi complessi e migliorano la manutenibilità.

Unioni Discriminatorie: Utilizzare una Proprietà Letterale Comune per Distinguere i Tipi

Un’unione discriminata è una potente funzionalità di TypeScript che combina più tipi in uno, utilizzando una proprietà comune per discriminare tra i tipi.

Esempio: Unione Discriminata con un Tipo Letterale

interface Dog {
  type: "dog";
  bark(): void;
}

interface Cat {
  type: "cat";
  meow(): void;
}

type Animal = Dog | Cat;

function makeSound(animal: Animal) {
  if (animal.type === "dog") {
    animal.bark();
  } else {
    animal.meow();
  }
}

In questo caso, la proprietà di tipo è la discriminante che TypeScript utilizza per restringere il tipo all’interno dell’unione Animal. Questo rende il lavoro con più tipi molto più semplice e sicuro dal punto di vista del tipo.

Conclusione

Padroneggiare le funzionalità avanzate di TypeScript ti consente di scrivere codice altamente flessibile e sicuro per applicazioni complesse. Rivediamo le caratteristiche principali che abbiamo esplorato:

Incorporare queste funzionalità avanzate nel tuo flusso di lavoro TypeScript renderà il tuo codice più potente, manutenibile e resistente agli errori. Con la pratica, questi strumenti ti permetteranno di affrontare anche gli scenari più complessi con sicurezza.