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 tipox?" → "il tipo diyrientra nel tipo dix?"
Questa domanda può presentarsi in una chiamata di funzione:
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,
// il valore contenuto in y è equivalente a ciò che x permette?
x = y;o in un valore di ritorno di una funzione:
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":
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":
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: ConsoleThe `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
```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.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à
marcadi tipostring. - Una proprietà
modellodi tipostring. - Una proprietà
annodi tiponumber.
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...
{ 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 tipox?" → "il tipo diyrientra nel tipo dix?"
in TypeScript, questo si traduce in:
"il tipo
yè equivalente al tipox?" → "l'insieme rappresentato dayè un sottoinsieme dell'insieme rappresentato dax?"
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
-
Tra queste ci sono: lo
yielddelle funzioni generatrici, e le convenzioni basate su proprietà accessor/mutator. ↩
