Pompei Tech
Guide · typescript

Tipi strutturali vs nominali

Tipi strutturali vs nominali

C'è un vecchio detto in inglese, il "duck test": "se cammina come un'anatra, nuota come un'anatra e starnazza come un'anatra, allora probabilmente è un'anatra". Non ti serve il certificato di nascita dell'animale per classificarlo: ti basta osservare cosa fa e come si comporta. Ora confrontalo con un altro scenario: l'accesso a un'area riservata di un aeroporto, dove non basta "comportarsi" da personale autorizzato — serve un badge specifico, emesso da un'autorità specifica, con un nome specifico sopra. Puoi indossare la stessa divisa, avere lo stesso comportamento, ma senza quel badge preciso, non entri.

Questi due scenari rappresentano perfettamente le due grandi famiglie di sistemi di tipi che vedremo in questo capitolo — ed è importante capire bene la differenza, perché TypeScript si comporta come il duck test, non come il controllo badge, e questo sorprende spesso chi arriva da linguaggi come Java o C#.

Modi di categorizzare i sistemi di tipi

Facciamo un passo indietro e pensiamo ad alcuni aspetti concettuali dei tipi e dei sistemi di tipi. In cosa TypeScript è simile e diverso da Java e da JavaScript?

Cos'è il type checking?

Come abbiamo già visto, il type checking può essere pensato come un compito che cerca di rispondere alla domanda di compatibilità o equivalenza di tipo.

"il tipo y è equivalente al tipo x?" → "il tipo di y rientra nel tipo di x?"

Questa domanda può presentarsi in una chiamata di funzione:

ts
function foo(x: unknown) {
  // ... codice misterioso ...
}

const mioValore: any = {};
//
// TYPE CHECKING
// -------------
// mioValore è equivalente a
// ciò che foo si aspetta di ricevere?
foo(mioValore);

in un'assegnazione,

ts
// il valore contenuto in y è equivalente a ciò che x permette?
x = y;

o in un valore di ritorno di una funzione:

ts
function bar(): string[] {
  if (Math.random() > 0.5) return [];
  // TYPE CHECKING
  // -------------
  // ["a"] è equivalente a
  // ciò che bar dichiara di restituire?
  return ["a"];
}

così come in qualche altra situazione più particolare1.

Statico vs dinamico

Classificare i sistemi di tipi come statici o dinamici ha a che fare con se il type checking viene eseguito a compile time oppure no.

Il sistema di tipi di TypeScript è statico.

Java, C# e C++ rientrano tutti in questa categoria. Tieni presente che l'inferenza può comunque avvenire nei sistemi di tipi statici — TypeScript, Scala e Haskell hanno tutti qualche forma di type checking statico.

I sistemi di tipi dinamici eseguono la loro valutazione di "equivalenza di tipo" puramente a runtime. JavaScript, Python, Ruby, Perl e PHP rientrano in questa categoria, anche se esistono progetti interessanti come Sorbet (Ruby) e Mypy (Python) che portano il type checking statico anche a questi linguaggi.

Duck typing

Il "duck typing" prende il nome proprio dal duck test che abbiamo citato all'inizio del capitolo. In pratica, questo approccio è molto simile alla tipizzazione strutturale (di cui parliamo tra poco), ma il termine "duck typing" viene solitamente usato per descrivere sistemi di tipi dinamici.

Tipi "forti" vs "deboli"

Questi termini, per quanto usati spesso, non hanno una definizione tecnica condivisa. Nel contesto di TypeScript, chi dice "fortemente tipizzato" spesso intende semplicemente "staticamente tipizzato".

Nominale vs strutturale

I sistemi di tipi nominali riguardano principalmente i nomi. Diamo un'occhiata a un semplice esempio in Java — il nostro "controllo badge in aeroporto":

java
public class Auto {
  String marca;
  String modello;
  int anno;
}

public class ControlloreAuto {
  // accetta un argomento di tipo Auto, restituisce una String
  public static String controllaAuto(Auto auto) {  }
}

Auto miaAuto = new Auto();
// TYPE CHECKING
// -------------
// miaAuto è equivalente a
// ciò che controllaAuto si aspetta come argomento?
ControlloreAuto.controllaAuto(miaAuto);

Nel codice sopra, quando si valuta la questione dell'equivalenza di tipo sull'ultima riga, tutto ciò che conta è se miaAuto è un'istanza della classe chiamata Auto. Non importa quali proprietà ha, importa solo il nome della classe da cui proviene.

Il sistema di tipi di TypeScript è strutturale

I sistemi di tipi strutturali riguardano principalmente la struttura, o la "forma". Diamo un'occhiata a un esempio TypeScript — il nostro "duck test":

ts
class class AutoAuto {
  Auto.marca: stringmarca: string = "";
  Auto.modello: stringmodello: string = "";
  Auto.anno: numberanno: number = 0;
  Auto.isElettrica: booleanisElettrica: boolean = false;
}

class class CamionCamion {
  Camion.marca: stringmarca: string = "";
  Camion.modello: stringmodello: string = "";
  Camion.anno: numberanno: number = 0;
  Camion.capacitaTraino: numbercapacitaTraino: number = 0;
}

const 
const veicolo: {
    marca: string;
    modello: string;
    anno: number;
}
veicolo
= {
marca: stringmarca: "Honda", modello: stringmodello: "Accord", anno: numberanno: 2017, }; function
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
}): void
stampaAuto
(
auto: {
    marca: string;
    modello: string;
    anno: number;
}
auto
: {
marca: stringmarca: string; modello: stringmodello: string; anno: numberanno: number; }) { 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
(`${
auto: {
    marca: string;
    modello: string;
    anno: number;
}
auto
.marca: stringmarca} ${
auto: {
    marca: string;
    modello: string;
    anno: number;
}
auto
.modello: stringmodello} (${
auto: {
    marca: string;
    modello: string;
    anno: number;
}
auto
.anno: numberanno})`);
} // Nessuno di questi tre è un errore, nonostante Auto, Camion e "veicolo" // non abbiano nulla in comune se non la forma richiesta da stampaAuto.
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
}): void
stampaAuto
(new constructor Auto(): AutoAuto());
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
}): void
stampaAuto
(new constructor Camion(): CamionCamion());
function stampaAuto(auto: {
    marca: string;
    modello: string;
    anno: number;
}): void
stampaAuto
(
const veicolo: {
    marca: string;
    modello: string;
    anno: number;
}
veicolo
);

Alla funzione stampaAuto non importa da quale costruttore proviene il suo argomento, le importa solo che abbia:

  • Una proprietà marca di tipo string.
  • Una proprietà modello di tipo string.
  • Una proprietà anno di tipo number.

Se l'argomento che le viene passato soddisfa questi requisiti, stampaAuto è contenta — non le interessa se hai chiamato la classe Auto, Camion, o se non è nemmeno un'istanza di classe, ma un semplice oggetto letterale.

Tornando alla nostra discussione sui tipi come insiemi di valori, l'argomento passato a stampaAuto ha un tipo che potremmo descrivere come...

text
{ tutti i valori che contengono una...
      proprietà marca    che è una string
  e una proprietà modello che è una string
  e una proprietà anno    che è un number
}

Dato che ogni istanza di Auto e ogni istanza di Camion soddisfano questo criterio, gli insiemi {tutte le possibili istanze di Auto} e {tutte le possibili istanze di Camion} sono entrambi sottoinsiemi di questo insieme.

Tornando al concetto iniziale:

"il tipo y è equivalente al tipo x?" → "il tipo di y rientra nel tipo di x?"

in TypeScript, questo si traduce in:

"il tipo y è equivalente al tipo x?" → "l'insieme rappresentato da y è un sottoinsieme dell'insieme rappresentato da x?"

Questa è la ragione per cui, arrivando da Java o C#, la prima volta che vedi TypeScript accettare un oggetto letterale al posto di un'istanza di classe ti sembra "strano" — in un sistema di tipi nominale semplicemente non potrebbe succedere. In TypeScript, invece, è il comportamento previsto e voluto: conta la forma, non il nome.

Footnotes

  1. Tra queste ci sono: lo yield delle funzioni generatrici, e le convenzioni basate su proprietà accessor/mutator. ↩