CAPITOLO 3

Basi di TypeScript

Principiante
~30 min
1h 45m

TypeScript vs JavaScript

JavaScript è un linguaggio di programmazione utilizzato principalmente per lo sviluppo di applicazioni web. TypeScript è un superset di JavaScript, il che significa che include tutte le funzionalità di JavaScript e ne aggiunge alcune nuove.

Mentre JavaScript funziona direttamente nel browser, TypeScript necessita di essere compilato per poter funzionare.

In TypeScript è necessario specificare il tipo di dati di ogni variabile (e struttura dati in generale), mentre in JavaScript non è richiesto. Questo può rendere il codice TypeScript più leggibile e più facile da mantenere, poiché gli errori di tipo possono essere individuati in fase di compilazione anziché in esecuzione.

TypeScript offre alcune funzionalità avanzate, come la possibilità di definire interfacce e classi, che non sono disponibili in JavaScript vanilla. Queste funzionalità possono rendere il codice TypeScript più organizzato e modulare.

In sintesi, TypeScript è un'estensione di JavaScript che offre una maggiore type safety e funzionalità avanzate per lo sviluppo di applicazioni web.

I Tipi

In TypeScript, i tipi di dati possono essere suddivisi in due categorie: tipi primitivi e tipi astratti.

Tipi primitivi

I tipi primitivi sono i tipi di base del linguaggio:

TipoDescrizione
numberPer i numeri (interi o float)
stringPer le stringhe di caratteri
booleanPer i valori vero/falso
nullPer il valore null
undefinedPer il valore undefined

null è un valore assegnabile a una variabile che indica l'assenza intenzionale di un valore. Viene utilizzato per indicare che una variabile è vuota o non ha un valore valido.

undefined indica che una variabile non è stata inizializzata o che non ha un valore assegnato.

In pratica, null viene assegnato esplicitamente da un programmatore per indicare l'assenza di un valore, mentre undefined viene assegnato implicitamente dal linguaggio quando una variabile non ha un valore assegnato.

Tipi astratti

I tipi astratti sono tipi che possono essere utilizzati per descrivere un insieme più ampio di valori:

TipoDescrizione
anyLa variabile può assumere qualsiasi tipo di valore
unknownIl tipo è sconosciuto fino a quando non viene utilizzato
objectLa variabile è un oggetto (non un tipo primitivo)
arrayLa variabile è un array di elementi di un certo tipo
functionLa variabile è una funzione (un riferimento ad una funzione)

Il tipo unknown è simile al tipo any, ma con una restrizione più forte. Mentre il tipo any consente di eseguire operazioni arbitrarie senza alcun controllo di tipo, il tipo unknown richiede una verifica di tipo prima di poter eseguire operazioni specifiche.

Dichiarare le variabili

Quando si dichiara una variabile usando const, var o let, si può aggiungere un'annotazione di tipo per specificare esplicitamente il tipo della variabile:

const _miaCostante: number = 100;
let _miaString: string = "Hello string!";
var _mioBooleano: boolean = true;

console.log(_miaCostante, _miaString, _mioBooleano);

In estrema sintesi:

  • var è usato per variabili globali (quindi non lo useremo mai)
  • let per variabili locali
  • const per le costanti

Utilizziamo il comando console.log() per stampare nella console i valori delle variabili dichiarate nel nostro codice. Come potete vedere nell'esempio, console.log() può essere utilizzato per visualizzare più valori contemporaneamente separandoli con la virgola.

Operatori

In TypeScript, ci sono diversi tipi di operatori che possono essere utilizzati per eseguire operazioni su variabili e valori.

Operatori aritmetici

OperatoreDescrizione
+Addizione
-Sottrazione
*Moltiplicazione
/Divisione
%Modulo (resto della divisione)

Operatori di confronto

OperatoreDescrizione
==Uguaglianza
===Uguaglianza rigorosa
!=Diverso
!==Diverso rigoroso
>Maggiore di
<Minore di
>=Maggiore o uguale a
<=Minore o uguale a

Operatori logici

OperatoreDescrizione
&&AND logico
||OR logico
!NOT logico

Operatori di assegnazione

OperatoreDescrizione
=Assegnazione
+=Assegnazione con addizione
-=Assegnazione con sottrazione
*=Assegnazione con moltiplicazione
/=Assegnazione con divisione

Operatori di incremento/decremento

OperatoreDescrizione
++Incremento
--Decremento

Operatori di accesso

OperatoreDescrizione
.Accesso alle proprietà di un oggetto
[]Accesso agli elementi di un array

Costrutti condizionali

In TypeScript, i costrutti condizionali sono utilizzati per eseguire determinate azioni in base a condizioni specifiche.

IF

Esegue un determinato blocco di codice solo se la condizione specificata è vera:

let x: number = 0;

if (x == 0) {
  console.log("Il valore della variabile x è: " + x);
}

IF ELSE

Esegue un blocco di codice se la condizione è vera e un altro blocco se la condizione è falsa:

let y: number = 0;

if (y == 0) {
  console.log("Il valore di y è zero");
} else {
  console.log("Il valore di y non è zero");
}

IF ELSE IF

Esegue blocchi diversi in base a condizioni multiple:

let z: number = 0;

if (z == 0) {
  console.log("Il valore di z è zero");
} else if (z == 1) {
  console.log("Il valore di z è uno");
} else {
  console.log("Il valore di z è diverso da 0 e da 1");
}

SWITCH

Quando siamo di fronte a diverse alternative, un if a cascata può risultare difficile da leggere. In questi casi possiamo ricorrere all'istruzione switch:

let q: number = 0;

switch (q) {
  case 0:
    console.log("Il valore di q è zero");
    break;

  case 1:
    console.log("Il valore di q è uno");
    break;

  default:
    console.log("Il valore di q è diverso da 1 e 0");
    break;
}

È importante inserire l'istruzione break al termine del blocco di istruzioni per evitare la prosecuzione con il caso successivo. La presenza di un caso default è opzionale.

Gli Array

Gli array in TypeScript sono un tipo di dato che permette di contenere una sequenza di elementi di un determinato tipo.

Dichiarazione

// Array di interi
let numArr1: number[] = [1, 2, 3, 4];
let numArr2: Array<number> = [1, 2, 3, 4];

// Array di stringhe
let strArr1: string[] = ['a', 'b', 'c'];
let strArr2: Array<string> = ['a', 'b', 'c'];

// Array misto
let myArr: Array<string | number> = ['a', 2, 'c'];

La sintassi // ci permette di inserire i commenti nel nostro codice. Possiamo anche usare /* */ per inserire commenti più lunghi.

Accesso agli elementi

Per accedere ai valori di un array, utilizza l'operatore [] e passa l'indice del valore:

console.log(numArr1[0]); // stampa 1
console.log(strArr2[1]); // stampa 'b'

L'indice degli Array parte da 0, non da 1!

Iterare un Array

numArr1.forEach((valore: number) => {
  console.log(valore);
});
// stampa 1, 2, 3, 4

Aggiungere elementi

// push aggiunge alla fine
numArr1.push(5);        // [1, 2, 3, 4, 5]
numArr1.push(6, 7);     // [1, 2, 3, 4, 5, 6, 7]

// unshift aggiunge all'inizio
numArr1.unshift(-1);    // [-1, 1, 2, 3, 4, 5, 6, 7]
numArr1.unshift(-2, -3);// [-2, -3, -1, 1, 2, 3, 4, 5, 6, 7]

Rimuovere elementi

// pop rimuove l'ultimo elemento
numArr1.pop();   // [-2, -3, -1, 1, 2, 3, 4, 5, 6]

// shift rimuove il primo elemento
numArr1.shift(); // [-3, -1, 1, 2, 3, 4, 5, 6]

Lo Spread Operator

Lo spread operator si scrive con tre puntini ... messi prima di un array o di un oggetto. Il suo compito è semplice: "spacchetta" tutti gli elementi che contiene, uno per uno.

Pensalo come rovesciare una busta di caramelle sul tavolo: invece di passare la busta intera, tiri fuori ogni singola caramella e la metti dove vuoi.

Copiare un array

let originali: number[] = [1, 2, 3];
let copia: number[] = [...originali];

copia.push(4);

console.log(originali); // [1, 2, 3]  -> non cambia!
console.log(copia);     // [1, 2, 3, 4]

Senza ..., scrivendo let copia = originali, copia sarebbe solo un altro nome per lo stesso array: modificarne uno modificherebbe anche l'altro. Con lo spread operator invece creiamo un array nuovo e indipendente.

Unire più array

let nemici: string[] = ['goomba', 'koopa'];
let boss: string[] = ['bowser'];

let tuttiIPersonaggi: string[] = [...nemici, ...boss, 'mario'];

console.log(tuttiIPersonaggi); // ['goomba', 'koopa', 'bowser', 'mario']

Possiamo unire due (o più) array, e aggiungere anche elementi extra, semplicemente elencandoli tra le parentesi quadre.

Copiare e modificare un oggetto

Lo spread operator funziona anche con gli oggetti, usando le parentesi graffe {}:

let giocatore = { nome: 'Mario', vite: 3, moneteRaccolte: 10 };

// creo un nuovo oggetto uguale al primo, ma con vite aggiornate
let giocatoreDopoDanno = { ...giocatore, vite: 2 };

console.log(giocatore);          // { nome: 'Mario', vite: 3, moneteRaccolte: 10 }
console.log(giocatoreDopoDanno); // { nome: 'Mario', vite: 2, moneteRaccolte: 10 }

Le proprietà scritte dopo lo spread sovrascrivono quelle originali. Questo è utilissimo per aggiornare lo stato di un gioco senza modificare l'oggetto di partenza.

Passare elementi di un array come parametri di una funzione

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

let numeri: number[] = [5, 10, 15];

console.log(sommaTre(...numeri)); // stampa 30

Invece di scrivere sommaTre(numeri[0], numeri[1], numeri[2]), lo spread operator "spacchetta" l'array direttamente nei tre parametri della funzione.

Ricorda: lo spread operator non modifica l'array o l'oggetto originale, ne crea sempre uno nuovo. È lo strumento perfetto quando in un gioco vuoi salvare uno "stato precedente" prima di applicare un cambiamento (ad esempio prima di aggiornare punteggio, vite o inventario del giocatore).

Le funzioni

Le funzioni sono un insieme riutilizzabile di istruzioni che eseguono un compito specifico. Possono avere dei parametri in ingresso e restituire un output.

function sayHello(): void {
  console.log("hello!");
}

function sumValues(valueOne: number, valueTwo: number): number {
  return valueOne + valueTwo;
}

La prima funzione (sayHello) non ha parametri di input e non restituisce nessun dato, quindi il tipo restituito è void. Nel secondo caso (sumValues) la funzione accetta dei parametri tipizzati e restituisce un number.

Se proviamo ad invocare la funzione con valori che non siano numerici, otterremo un errore da parte del compilatore TypeScript.

Per richiamare le funzioni:

sayHello();
console.log(sumValues(1, 1)); // stampa 2

Arrow Functions

Le arrow function sono una sintassi abbreviata per la creazione di funzioni anonime. Utilizzano il simbolo => (freccia) per separare gli argomenti dal corpo:

const mySum = (a: number, b: number) => a + b;

Questa arrow function è equivalente a:

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

Le arrow function hanno alcune proprietà interessanti:

  • Non hanno il loro proprio this, quindi il valore di this viene ereditato dal contesto in cui vengono chiamate
  • Non possono essere utilizzate come costruttori (non possono essere chiamate con new)

Enumerati (Enum)

Gli enumerati sono un tipo di dato che permette di assegnare un nome (chiave) a un insieme di valori numerici o stringhe. È come creare un set di costanti con dei valori specifici.

enum Directions {
  top,    // valore 0
  bottom, // valore 1
  left,   // valore 2
  right   // valore 3
}

Per default ad ogni nome viene assegnato un valore numerico incrementale a partire da 0. È possibile anche indicare un valore di partenza diverso:

enum Directions {
  top = 1,  // valore 1
  bottom,   // valore 2
  left,     // valore 3
  right     // valore 4
}

Esempio pratico

let playerDirection: Directions;
playerDirection = Directions.top;

function checkPlayerDirection(direction: Directions): void {
  switch (direction) {
    case Directions.bottom:
      console.log("Direzione: basso");
      break;
    case Directions.top:
      console.log("Direzione: alto");
      break;
    case Directions.left:
      console.log("Direzione: sinistra");
      break;
    case Directions.right:
      console.log("Direzione: destra");
      break;
  }
}

checkPlayerDirection(playerDirection);

È anche possibile assegnare valori specifici per ogni chiave:

enum PlayerHP {
  hero = 1000,
  general = 800,
  captain = 500,
  soldier = 100
}

Interfacce

Le interfacce in TypeScript sono un modo per definire un contratto che specifica quali proprietà e metodi devono avere un oggetto o una classe.

interface IPerson {
  name: string;
  age?: number;
  greet?: (message: string) => void;
}

È buona norma inserire una I maiuscola prima del nome dell'interfaccia. Il ? dopo una proprietà identifica che quella proprietà è opzionale.

Una volta dichiarata un'interfaccia, possiamo utilizzarla:

const mario: IPerson = { name: "Francesco", age: 51 };

Qui abbiamo fatto qualcosa di "magico" rispetto agli altri linguaggi di programmazione: abbiamo creato un oggetto partendo da un'interfaccia (senza usare una classe).

Classi e ereditarietà

In programmazione ad oggetti, una classe è un modello per un oggetto che definisce le sue proprietà e il suo comportamento. Una classe viene utilizzata per creare istanze di oggetti che hanno le stesse caratteristiche e comportamenti.

Una classe può avere:

  • Attributi (proprietà): descrivono le caratteristiche dell'oggetto
  • Metodi (funzioni): descrivono le azioni che l'oggetto può compiere
class Person {
  name: string;
  age: number;

  greet(message: string): void {
    console.log("Ciao sono " + this.name + " e il mio messaggio è: " + message);
  }
}

Ereditarietà

Le classi possono essere organizzate in gerarchie, dove una classe può estendere un'altra classe ed ereditarne le proprietà e il comportamento:

class Student extends Person {
  instagram: string;

  share(): void {
    console.log("Il mio instagram è: " + this.instagram);
  }
}

Un'istanza di Student avrà tutti gli attributi di un'istanza di Person, oltre a instagram e al metodo share().

I metodi delle classi non hanno bisogno della parola riservata function come le funzioni standalone.

Il costruttore

Il costruttore di una classe è un metodo speciale che viene chiamato quando viene creata un'istanza della classe. Viene utilizzato per inizializzare le proprietà dell'oggetto.

class Person {
  name: string;
  age: number;

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

  greet(message: string): void {
    console.log("Ciao sono " + this.name + " e il mio saluto è: " + message);
  }
}

Per creare un'istanza della classe:

let me: Person = new Person("Francesco", 51);
me.greet("Hello!"); // stampa "Ciao sono Francesco e il mio saluto è: Hello!"
console.log(me.name); // stampa "Francesco"

Classi e interfacce

In TypeScript, le classi possono implementare interfacce per assicurarsi che abbiano le proprietà e i metodi specificati:

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

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

  greet(message: string): void {
    console.log("Ciao sono " + this.name + " e il mio saluto è: " + message);
  }
}

Se creassimo la classe Person senza inserire la proprietà name, il compilatore ci segnalerebbe immediatamente un errore del tipo: Class 'Person' incorrectly implements interface 'IPerson'.

Public, Private e Protected

In TypeScript, è possibile usare le parole riservate public, private e protected per specificare il livello di accessibilità di proprietà e metodi:

ModificatoreAccessibilità
publicAccessibile da qualsiasi parte del codice (default)
privateAccessibile solo all'interno della classe
protectedAccessibile dalla classe e dalle sue sottoclassi

Getter e Setter

Possiamo usare get e set per accedere o settare il valore delle proprietà private:

class Person implements IPerson {
  private _name: string;
  private _age: number;

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

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._name = name;
  }

  get age(): number {
    return this._age;
  }

  set age(age: number) {
    this._age = age;
  }

  greet(message: string): void {
    console.log("Ciao sono " + this.name + " e il mio saluto è: " + message);
  }
}

Questa metodologia ci permette di avere un maggiore controllo sulle proprietà della nostra classe, ad esempio validando i valori prima di assegnarli.

Import ed Export

TypeScript (come JavaScript moderno) organizza il codice in moduli: ogni file .ts è un modulo separato. Per condividere classi, funzioni e variabili tra file diversi si usano le parole chiave export e import.

Export

export rende disponibile all'esterno un elemento del modulo. Esistono due forme principali:

Export con nome — si possono esportare più elementi dallo stesso file:

// nel file GameData.ts
export const MAX_LIVES: number = 3;
export const GAME_WIDTH: number = 800;

export interface IGameConfig {
  width: number;
  height: number;
}

Export di default — ogni file può avere un solo export default. Viene usato tipicamente per le classi principali del file:

// nel file scenes/Boot.ts
export default class Boot extends Phaser.Scene {
  // ...
}

Import

import permette di utilizzare all'interno di un file gli elementi esportati da un altro.

Per importare un export di default si sceglie liberamente il nome locale:

import Boot from "./scenes/Boot";
import GamePlay from "./scenes/GamePlay";

Per importare export con nome si usano le parentesi graffe {}. Il nome deve corrispondere esattamente a quello esportato:

import { GameData } from "./GameData";
import { MAX_LIVES, GAME_WIDTH } from "./GameData";

Si possono combinare i due stili nello stesso import:

import Phaser, { Types } from "phaser";

Il percorso dopo from può essere relativo ("./scenes/Boot", "../GameData") oppure il nome di un pacchetto npm installato ("phaser"). TypeScript aggiunge automaticamente l'estensione .ts ai percorsi relativi.

Esempio dal template

Nel file index.ts del nostro template trovi entrambe le forme in azione:

// import da pacchetto npm (default)
import "phaser";

// import di classi con export default da file locali
import Boot from "./scenes/Boot";
import Preloader from "./scenes/Preloader";
import GamePlay from "./scenes/GamePlay";

// import di export con nome
import { GameData } from "./GameData";

E nel file GameData.ts:

// export con nome di un oggetto
export const GameData = {
  globals: {
    gameWidth: 800,
    gameHeight: 600,
    bgColor: "#000000",
    debug: false,
  },
};

Grazie ai moduli, ogni scena del gioco vive nel suo file e viene importata solo dove serve. Questo rende il codice più organizzato e facilita la collaborazione in team.

In questo capitolo abbiamo:

  • ✅ Compreso le differenze tra TypeScript e JavaScript
  • ✅ Esplorato i tipi primitivi e astratti
  • ✅ Imparato a dichiarare variabili con i tipi
  • ✅ Utilizzato operatori e costrutti condizionali
  • ✅ Lavorato con gli Array
  • ✅ Creato funzioni e arrow functions
  • ✅ Definito enumerati e interfacce
  • ✅ Compreso classi, ereditarietà e costruttori
  • ✅ Scoperto i modificatori di accesso
  • ✅ Imparato a usare import ed export per organizzare il codice in moduli

Per un approfondimento su TypeScript è possibile consultare il manuale ufficiale: typescriptlang.org/docs/handbook

Nel prossimo capitolo inizieremo a configurare il nostro gioco con Phaser 3 e a esplorare il sistema a scene.

Sh4rk0

Sì, lo so. Questo capitolo sembrava quell'esame universitario per cui non ti senti mai abbastanza pronto. Tipi, interfacce, classi, getter, setter, import, export... è tanto.

Ma sai una cosa? Nei prossimi capitoli userai il 30% di tutto questo per l'80% del tempo. Il resto lo rileggerai qui quando ne avrai bisogno — e averne bisogno è normale.

Quello che devi portarti a casa adesso è questo: TypeScript ti urla in faccia prima che il gioco parta quando fai qualcosa di sbagliato. Questo è un superpotere, non una scocciatura. Le classi hanno un costruttore, proprietà private con _ davanti, e metodi che fanno le cose. Import ed export tengono tutto organizzato.

Il resto viene da sé con la pratica. Promesso. 💪