Pompei Tech
Guide · typescript

Oggetti, array e tuple

Oggetti, array e tuple

Pensa a un modulo di spedizione di un pacco. Ha dei campi che ci sono sempre — mittente, destinatario, indirizzo — e magari un campo "note per il corriere" che a volte è compilato e a volte no. Chiunque riceva quel modulo sa esattamente cosa aspettarsi: sa quali informazioni troverà sempre, e quali potrebbero mancare. Ora immagina di ricevere pacchi con moduli scritti a mano, ognuno con campi diversi, in ordine diverso, alcuni con informazioni extra scarabocchiate a margine. Il caos è garantito.

Ora che sappiamo tipizzare variabili e funzioni semplici, rendiamo le cose un po' più interessanti tipizzando le collezioni: in JavaScript questo significa principalmente oggetti e array.

Oggetti

In generale, i tipi degli oggetti sono definiti da:

  • I nomi delle proprietà che sono (o possono essere) presenti.
  • I tipi di quelle proprietà.

Per esempio, se avessimo il concetto di un'Auto come "Toyota Corolla 2002" con queste proprietà:

  • marca: il produttore (in questo caso, "Toyota").
  • modello: il prodotto specifico (in questo caso, "Corolla").
  • anno: l'anno del modello (in questo caso, 2002).

Potremmo creare un oggetto JavaScript per rappresentare questa informazione:

js
{
  marca: "Toyota",
  modello: "Corolla",
  anno: 2002
}

Il tipo che descriverebbe la struttura di questo oggetto:

ts
{
  marca: string;
  modello: string;
  anno: number;
}

Possiamo usare questo tipo con una variabile, con la stessa notazione : tipo che abbiamo già visto!

ts
let auto: {
  marca: string;
  modello: string;
  anno: number;
};

Potremmo creare una funzione per stampare in console i valori di questo tipo:

ts
/**
 * Stampa in console le informazioni su un'auto
 * @param auto - l'auto da stampare
 */
function stampaAuto(auto: {
  marca: string;
  modello: string;
  anno: number;
}) {
  console.log(`${auto.marca} ${auto.modello} (${auto.anno})`);
}

Nota che possiamo usare esattamente lo stesso tipo di annotazione per gli argomenti di una funzione. A questo punto, se scrivi auto. all'interno del corpo di questa funzione, il tuo editor ti mostrerà subito il completamento automatico con marca, modello e anno — niente di magico, è semplicemente il sistema di tipi al lavoro.

Proprietà opzionali

Cosa succede se spingiamo un po' oltre il nostro esempio dell'auto, aggiungendo una quarta proprietà che è presente solo a volte?

Nome proprietàÈ presenteTipoNota
marcaSemprestring
modelloSemprestring
annoSemprenumber
tensioneRicaricaA voltenumberassente a meno che l'auto sia elettrica

Possiamo dichiarare che questa proprietà è opzionale usando l'operatore ?:

ts
function 
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}): void
stampaAuto
(
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
: {
marca: stringmarca: string; modello: stringmodello: string; anno: numberanno: number; tensioneRicarica?: number | undefinedtensioneRicarica?: number; }) { let let str: stringstr = `${
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
.marca: stringmarca} ${
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
.modello: stringmodello} (${
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
.anno: numberanno})`;
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
.tensioneRicarica?: number | undefinedtensioneRicarica;
if (typeof
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
.tensioneRicarica?: number | undefinedtensioneRicarica !== "undefined")
let str: stringstr += ` // ${
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
.tensioneRicarica?: numbertensioneRicarica}v`;
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v22.x/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.
@sincev0.1.100
log
(let str: stringstr);
}

Nota che il tipo di tensioneRicarica è ora number | undefined. Approfondiremo il significato del simbolo | nel prossimo capitolo, ma per ora puoi considerarlo come un OR, per i tipi: number | undefined significa "o number, o undefined".

La nostra funzione stampaAuto ora funziona, sia che la proprietà tensioneRicarica sia presente o meno:

ts
// Funziona
stampaAuto({
  marca: "Honda",
  modello: "Accord",
  anno: 2017,
});

// Funziona anche questo
stampaAuto({
  marca: "Tesla",
  modello: "Model 3",
  anno: 2020,
  tensioneRicarica: 220,
});

Controllo delle proprietà in eccesso

TypeScript ci aiuta a intercettare un tipo particolare di problema legato all'uso di oggetti letterali. Vediamo la situazione in cui si presenta l'errore:

ts
function 
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}): void
stampaAuto
(
auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}
auto
: {
marca: stringmarca: string; modello: stringmodello: string; anno: numberanno: number; tensioneRicarica?: number | undefinedtensioneRicarica?: number; }) { // implementazione omessa per semplicità }
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
    tensioneRicarica?: number;
}): void
stampaAuto
({
marca: stringmarca: "Tesla", modello: stringmodello: "Model 3", anno: numberanno: 2020, colore: "ROSSO", // <-- PROPRIETÀ IN ECCESSO
Object literal may only specify known properties, and 'colore' does not exist in type '{ marca: string; modello: string; anno: number; tensioneRicarica?: number | undefined; }'.
});

La parte importante di questo messaggio d'errore è: "Object literal may only specify known properties, and 'colore' does not exist in type...". In questa situazione, all'interno del corpo della funzione stampaAuto, non potremmo comunque accedere alla proprietà colore, dato che non fa parte del tipo dell'argomento! Staremmo quindi definendo una proprietà su questo oggetto senza avere alcuna speranza di poterla leggere in modo sicuro in seguito — ed è esattamente questo che TypeScript vuole evitarti.

:::tip Prova a risolverlo in tre modi diversi

  1. Rimuovi la proprietà colore dall'oggetto.
  2. Aggiungi colore: string al tipo dell'argomento della funzione.
  3. Crea una variabile che contenga questo valore, e passa poi la variabile alla funzione stampaAuto (in questo caso il controllo sulle proprietà in eccesso non scatta). :::

Index signature

A volte abbiamo bisogno di rappresentare un tipo per i dizionari, dove valori di un tipo coerente sono recuperabili tramite delle chiavi.

Consideriamo la seguente collezione di numeri di telefono:

ts
const numeri = {
  casa: { prefisso: "+39", area: "02", numero: "652-4515" },
  lavoro: { prefisso: "+39", area: "06", numero: "752-5856" },
  fax: { prefisso: "+39", area: "011", numero: "525-4357" },
};

Sembra chiaro che possiamo memorizzare numeri di telefono sotto una "chiave" — in questo caso casa, lavoro, fax, e potenzialmente altre parole a nostra scelta — e ogni numero di telefono è composto da tre stringhe.

Possiamo descrivere questo valore usando quella che si chiama index signature:

ts
const 
const numeri: {
    [k: string]: {
        prefisso: string;
        area: string;
        numero: string;
    };
}
numeri
: {
[k: stringk: string]: { prefisso: stringprefisso: string; area: stringarea: string; numero: stringnumero: string; }; } = {};
const numeri: {
    [k: string]: {
        prefisso: string;
        area: string;
        numero: string;
    };
}
numeri
.
numeri[string]: {
    prefisso: string;
    area: string;
    numero: string;
}
fax
;

Ora, indipendentemente da quale chiave cerchiamo, otteniamo sempre un oggetto che rappresenta un numero di telefono.

Tipi Array

Descrivere i tipi per gli array è spesso semplice quanto aggiungere [] alla fine del tipo dell'elemento contenuto. Per esempio, il tipo per un array di string si scrive string[]:

ts
const 
const estensioniFile: string[]
estensioniFile
= ["js", "ts"];

Puoi usare anche il nostro tipo Auto più complesso, seguendo il tipo del nostro oggetto a 3 proprietà con []:

ts
const 
const auto: {
    marca: string;
    modello: string;
    anno: number;
}[]
auto
= [
{ marca: stringmarca: "Toyota", modello: stringmodello: "Corolla", anno: numberanno: 2002, }, ];

Tuple

A volte vogliamo lavorare con una struttura dati ordinata, a più elementi, dove la posizione di ogni elemento ha un significato o una convenzione specifica. Questo tipo di struttura si chiama spesso tupla.

Immaginiamo di definire una convenzione dove rappresentiamo la stessa "Toyota Corolla 2002" come:

ts
//              [Anno, Marca,    Modello  ]
let miaAuto = [2002, "Toyota", "Corolla"];
// l'assegnazione con destrutturazione è comoda qui!
const [anno, marca, modello] = miaAuto;

Vediamo come TypeScript gestisce l'inferenza in questo caso:

ts
let 
let miaAuto: (string | number)[]
miaAuto
= [2002, "Toyota", "Corolla"];
const [const anno: string | numberanno,
const marca: string | number
marca
, const modello: string | numbermodello] = let miaAuto: (string | number)[]miaAuto;

| significa "OR", quindi possiamo pensare a string | number come o una stringa, o un numero.

TypeScript ha scelto il tipo più specifico che descrive l'intero contenuto dell'array. Questo non è esattamente quello che volevamo, in quanto:

  • Ci permette di rompere la nostra convenzione secondo cui l'anno viene sempre per primo.
  • Non ci aiuta con l'aspetto della "lunghezza finita" tipico delle tuple.
ts
let miaAuto = [2002, "Toyota", "Corolla"];
// nessuna protezione: né sulla convenzione, né sulla lunghezza!
miaAuto = ["Honda", 2017, "Accord", "Sedan"];

In questo caso, TypeScript potrebbe inferire miaAuto come una di due cose. Quale pensi sia l'assunzione più comunemente utile?

:::note Quale assunzione è più utile, nella maggior parte dei casi?

  1. [2002, "Toyota", "Corolla"] dovrebbe essere considerato un array misto di numeri e stringhe.
  2. [2002, "Toyota", "Corolla"] dovrebbe essere considerato una tupla di lunghezza fissa (3).

Rifletti: quale delle due usi più spesso, nella pratica? :::

Se TypeScript facesse un'assunzione più specifica mentre inferisce il tipo di miaAuto, ci intralcerebbe per gran parte del tempo... Non c'è un problema enorme qui, ma significa che dobbiamo dichiarare esplicitamente il tipo di una tupla ogni volta che ne definiamo una.

ts
let let miaAuto2: [number, string, string]miaAuto2: [number, string, string] = [2002, "Toyota", "Corolla"];

// ERRORE: non rispetta la convenzione
let miaAuto2: [number, string, string]miaAuto2 = ["Honda", 2017, "Accord"];
Type 'number' is not assignable to type 'string'.
Type 'string' is not assignable to type 'number'.
// ERRORE: troppi elementi miaAuto2 = [2017, "Honda", "Accord", "Sedan"];
Type '[number, string, string, string]' is not assignable to type '[number, string, string]'. Source has 4 element(s) but target allows only 3.
const [
const anno2: number
anno2
, const marca2: stringmarca2] = let miaAuto2: [number, string, string]miaAuto2;

Ora otteniamo errori esattamente dove ce li aspettiamo, e tutti i tipi funzionano come speravamo.

Tuple readonly

Le tuple sono semplicemente normali Array di JavaScript.

ts
const coppiaNumeri: [number, number] = [4, 5];

Questo impone un certo grado di limitazione su come le tuple possono essere tipizzate. Per esempio, un Array permette di aggiungere nuovi elementi con .push(...), permette .splice(...) e così via. A runtime questi metodi esistono comunque su ogni tupla, e i tipi lo riflettono.

TypeScript fornisce comunque parecchio supporto in fase di assegnazione:

ts
const const coppiaNumeri: [number, number]coppiaNumeri: [number, number] = [4, 5];
const tripletta: [number, number, number] = [7];
Type '[number]' is not assignable to type '[number, number, number]'. Source has 1 element(s) but target requires 3.

e vediamo qualcosa di interessante succedere con .length:

ts
const const coppiaNumeri: [number, number]coppiaNumeri: [number, number] = [4, 5];

[101, 102, 103].
Array<number>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length
;
const coppiaNumeri: [number, number]coppiaNumeri.
length: 2
length
;

ma non abbiamo alcuna protezione riguardo push e pop, che di fatto cambierebbero la lunghezza (e quindi il tipo) della tupla:

ts
coppiaNumeri.push(6); // [4, 5, 6]
coppiaNumeri.pop(); // [4, 5]
coppiaNumeri.pop(); // [4]
coppiaNumeri.pop(); // []

coppiaNumeri.length; // ⚠️ nessun errore di compilazione: il tipo dice ancora "2", ma a runtime è "0"

Se siamo d'accordo nel trattare questa tupla come "sola lettura", possiamo dichiararlo esplicitamente, ottenendo molta più sicurezza riguardo alla mutazione:

ts
const const coppiaSolaLettura: readonly [number, number]coppiaSolaLettura: readonly [number, number] = [4, 5];
const coppiaSolaLettura: readonly [number, number]coppiaSolaLettura.
length: 2
length
;
const coppiaSolaLettura: readonly [number, number]coppiaSolaLettura.push(6);
Property 'push' does not exist on type 'readonly [number, number]'.
const coppiaSolaLettura: readonly [number, number]coppiaSolaLettura.pop();
Property 'pop' does not exist on type 'readonly [number, number]'.