Pompei Tech
Guide · typescript

Type queries

Type queries

Immagina un sarto che deve confezionare un vestito identico a uno che hai già, ma di cui hai perso il modello originale. Cosa fa? Prende il capo esistente e ci ricava le misure: lunghezza delle maniche, circonferenza vita, altezza. Non gli serve un disegno tecnico scritto da qualcun altro — gli basta "misurare" il capo che ha già davanti per ricavarne la forma.

Le type query in TypeScript funzionano esattamente così: ci permettono di ottenere informazioni di tipo misurando valori o tipi già esistenti, invece di doverli riscrivere da zero a mano. È una capacità incredibilmente importante, in particolare quando lavori con librerie che magari non espongono le informazioni di tipo nel modo più comodo per te.

keyof

La type query keyof ci permette di ottenere un tipo che rappresenta tutte le chiavi delle proprietà di una data interfaccia — un po' come leggere l'indice di un libro per sapere quali capitoli contiene, senza doverli leggere tutti.

ts
type 
type NomiProprietaDiData = keyof Date
NomiProprietaDiData
= keyof Date;

Non tutte le chiavi sono string, quindi possiamo separare quelle che sono symbol da quelle che sono string usando l'operatore di intersezione (&).

Se ti ricordi un po' di geometria, può essere utile pensarla come una specie di prodotto scalare: quando usiamo l'operatore di intersezione, restiamo solo con la sotto-parte di keyof Date che è inclusa anche in string o symbol, rispettivamente.

ts
type type NomiProprietaDiData = keyof DateNomiProprietaDiData = keyof Date;

type 
type NomiProprietaStringaDiData = "toString" | "toDateString" | "toTimeString" | "toLocaleString" | "toLocaleDateString" | "toLocaleTimeString" | "valueOf" | "getTime" | "getFullYear" | "getUTCFullYear" | "getMonth" | "getUTCMonth" | "getDate" | "getUTCDate" | "getDay" | "getUTCDay" | "getHours" | "getUTCHours" | "getMinutes" | "getUTCMinutes" | "getSeconds" | "getUTCSeconds" | "getMilliseconds" | "getUTCMilliseconds" | "getTimezoneOffset" | "setTime" | "setMilliseconds" | "setUTCMilliseconds" | "setSeconds" | "setUTCSeconds" | "setMinutes" | "setUTCMinutes" | ... 11 more ... | "getVarDate"
NomiProprietaStringaDiData
= type NomiProprietaDiData = keyof DateNomiProprietaDiData & string;
type
type NomiProprietaSimboloDiData = typeof Symbol.toPrimitive
NomiProprietaSimboloDiData
= type NomiProprietaDiData = keyof DateNomiProprietaDiData & symbol;

Interessante! Questa proprietà Symbol.toPrimitive è l'unica non-stringa1.

typeof

La type query typeof ci permette di estrarre un tipo a partire da un valore — è esattamente il nostro sarto che prende le misure da un capo già esistente, invece di leggerle da un disegno tecnico. Un esempio:

ts
async function function principale(): Promise<void>principale() {
  const const rispostaApi: [Response, string]rispostaApi = await var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise
.PromiseConstructor.all<[Promise<Response>, Promise<string>]>(values: [Promise<Response>, Promise<string>]): Promise<[Response, string]> (+1 overload)
Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.
@paramvalues An array of Promises.@returnsA new Promise.
all
([
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
fetch
("https://example.com"),
var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise
.PromiseConstructor.resolve<string>(value: string): Promise<string> (+2 overloads)
Creates a new resolved promise for the provided value.
@paramvalue A promise.@returnsA promise whose internal state matches the provided promise.
resolve
("Bianco Titanio"),
]); type
type TipoRispostaApi = [Response, string]
TipoRispostaApi
= typeof const rispostaApi: [Response, string]rispostaApi;
}

Un uso comune di typeof è ottenere un tipo che rappresenta il "lato statico" di una classe (cioè: il costruttore, le proprietà static, e altre cose non presenti su un'istanza della classe):

ts
const 
const MioCostruttoreAjax: {
    new (): CSSRule;
    prototype: CSSRule;
    readonly STYLE_RULE: 1;
    readonly CHARSET_RULE: 2;
    readonly IMPORT_RULE: 3;
    readonly MEDIA_RULE: 4;
    readonly FONT_FACE_RULE: 5;
    readonly PAGE_RULE: 6;
    readonly NAMESPACE_RULE: 10;
    readonly KEYFRAMES_RULE: 7;
    readonly KEYFRAME_RULE: 8;
    readonly SUPPORTS_RULE: 12;
    readonly COUNTER_STYLE_RULE: 11;
    readonly FONT_FEATURE_VALUES_RULE: 14;
}
MioCostruttoreAjax
=
var CSSRule: {
    new (): CSSRule;
    prototype: CSSRule;
    readonly STYLE_RULE: 1;
    readonly CHARSET_RULE: 2;
    readonly IMPORT_RULE: 3;
    readonly MEDIA_RULE: 4;
    readonly FONT_FACE_RULE: 5;
    readonly PAGE_RULE: 6;
    readonly NAMESPACE_RULE: 10;
    readonly KEYFRAMES_RULE: 7;
    readonly KEYFRAME_RULE: 8;
    readonly SUPPORTS_RULE: 12;
    readonly COUNTER_STYLE_RULE: 11;
    readonly FONT_FEATURE_VALUES_RULE: 14;
}
The **`CSSRule`** interface represents a single CSS rule. [MDN Reference](https://developer.mozilla.org/docs/Web/API/CSSRule)
CSSRule
;
var CSSRule: {
    new (): CSSRule;
    prototype: CSSRule;
    readonly STYLE_RULE: 1;
    readonly CHARSET_RULE: 2;
    readonly IMPORT_RULE: 3;
    readonly MEDIA_RULE: 4;
    readonly FONT_FACE_RULE: 5;
    readonly PAGE_RULE: 6;
    readonly NAMESPACE_RULE: 10;
    readonly KEYFRAMES_RULE: 7;
    readonly KEYFRAME_RULE: 8;
    readonly SUPPORTS_RULE: 12;
    readonly COUNTER_STYLE_RULE: 11;
    readonly FONT_FEATURE_VALUES_RULE: 14;
}
The **`CSSRule`** interface represents a single CSS rule. [MDN Reference](https://developer.mozilla.org/docs/Web/API/CSSRule)
CSSRule
.type STYLE_RULE: 1STYLE_RULE; // proprietà statica
const
const mioAjax: CSSRule
mioAjax
= new var CSSRule: new () => CSSRule
The **`CSSRule`** interface represents a single CSS rule. [MDN Reference](https://developer.mozilla.org/docs/Web/API/CSSRule)
CSSRule
();

MioCostruttoreAjax, la classe (il costruttore), è di tipo typeof CSSRule, mentre le istanze sono di tipo CSSRule.

Indexed Access Types

Gli Indexed Access Types (tipi ad accesso indicizzato) forniscono un meccanismo per recuperare parte di un tipo array o oggetto tramite degli indici. Vediamo come funziona questo tipo di costrutto, e un paio di esempi pratici di dove potresti usarlo.

Al livello più semplice, questi tipi riguardano l'accesso a una parte di un altro tipo, tramite un indice, un po' come cercare una voce specifica nell'indice di un libro:

ts
interface Auto {
  Auto.marca: stringmarca: string;
  Auto.modello: stringmodello: string;
  Auto.anno: numberanno: number;
  
Auto.colore: {
    rosso: string;
    verde: string;
    blu: string;
}
colore
: {
rosso: stringrosso: string; verde: stringverde: string; blu: stringblu: string; }; } let
let coloreAuto: {
    rosso: string;
    verde: string;
    blu: string;
}
coloreAuto
: Auto["colore"];

In questa situazione, 'colore' è l'"indice".

L'indice che usi deve essere una "chiave" valida utilizzabile su un valore di tipo Auto. Qui sotto puoi vedere cosa succede se provi a violare questa regola:

ts
interface Auto {
  Auto.marca: stringmarca: string;
  Auto.modello: stringmodello: string;
  Auto.anno: numberanno: number;
  
Auto.colore: {
    rosso: string;
    verde: string;
    blu: string;
}
colore
: { rosso: stringrosso: string; verde: stringverde: string; blu: stringblu: string };
} let let coloreAuto: anycoloreAuto: Auto["qualcosa-che-non-esiste-su-auto"];
Property 'qualcosa-che-non-esiste-su-auto' does not exist on type 'Auto'.

Puoi anche addentrarti più in profondità nell'oggetto attraverso "accessi" multipli:

ts
interface Auto {
  Auto.marca: stringmarca: string;
  Auto.modello: stringmodello: string;
  Auto.anno: numberanno: number;
  
Auto.colore: {
    rosso: string;
    verde: string;
    blu: string;
}
colore
: { rosso: stringrosso: string; verde: stringverde: string; blu: stringblu: string };
} let
let componenteRossoColoreAuto: string
componenteRossoColoreAuto
: Auto["colore"]["rosso"];

...e puoi passare o "proiettare" uno union type (|) attraverso Auto come indice, purché ogni parte dello union type sia individualmente un indice valido:

ts
interface Auto {
  Auto.marca: stringmarca: string;
  Auto.modello: stringmodello: string;
  Auto.anno: numberanno: number;
  
Auto.colore: {
    rosso: string;
    verde: string;
    blu: string;
}
colore
: { rosso: stringrosso: string; verde: stringverde: string; blu: stringblu: string };
} let
let proprietaAuto: number | {
    rosso: string;
    verde: string;
    blu: string;
}
proprietaAuto
: Auto["colore" | "anno"];

Caso d'uso: il pattern "registro dei tipi"

Stiamo per toccare un concetto di cui non abbiamo ancora parlato, ma possiamo usare una definizione di base per capire questo esempio.

ts
declare module "./lib/registro" {}

Questa si chiama una dichiarazione di modulo, e ci permette di "impilare" tipi sopra a cose già esportate da un modulo ./lib/registro.ts. Ricorda, esiste una sola definizione dei tipi esportati da ./lib/registro.ts, quindi se la modifichiamo usando una dichiarazione di modulo, quella modifica avrà effetto su ogni punto in cui quei tipi vengono usati.

Ora, usiamo keyof, le dichiarazioni di modulo e quello che abbiamo appena imparato sulle interfacce aperte per risolvere un problema concreto.

Immagina di star costruendo una libreria di accesso ai dati per un'applicazione web. Parte di questo compito prevede la costruzione di una funzione che recupera diversi tipi di record dall'API di un utente. Vogliamo poter recuperare un record dato il nome del tipo di record e il suo ID, ma, in quanto autori della libreria, non conosciamo in anticipo i tipi specifici di cui un dato utente avrà bisogno.

ts
// Supponiamo che il nostro utente abbia configurato risorse come Libro e Rivista
//
// restituisce un Libro
recuperaRecord("libro", "lb_123");
// restituisce una Rivista
recuperaRecord("rivista", "rv_456");

// dovrebbe rifiutarsi di compilare
recuperaRecord("boh", "");

Il nostro progetto potrebbe avere una struttura di file come questa:

text
data/
  libro.ts       // Un modello per i record Libro
  rivista.ts     // Un modello per i record Rivista
lib/
  registro.ts    // Il nostro registro dei tipi, e una funzione recuperaRecord
index.ts         // Punto di ingresso

Concentriamoci sul primo argomento della funzione recuperaRecord. Possiamo creare un'interfaccia "registro" che chiunque usi questa libreria può usare per "installare" i propri tipi di risorsa, e definire la funzione recuperaRecord usando la nostra nuova type query keyof.

ts
// lib/registro.ts
export interface RegistroTipiDati {
  // vuoto di proposito
}
// il "& string" è solo un trucco per ottenere un
// tooltip più leggibile nel prossimo passaggio
export function recuperaRecord(
  arg: keyof RegistroTipiDati & string,
  id: string,
) {}

Ora spostiamo l'attenzione verso il "codice applicativo". Definiremo le classi Libro e Rivista e le "registreremo" con l'interfaccia RegistroTipiDati, per poi vedere cosa succede al primo argomento della funzione recuperaRecord: diventerà "libro" | "rivista", nonostante la libreria non abbia assolutamente nulla nel suo codice che faccia riferimento a questi concetti per nome!

ts
// @filename: lib/registro.ts
export interface RegistroTipiDati {
  // vuoto di proposito
}
export function function recuperaRecord(arg: keyof RegistroTipiDati & string, id: string): voidrecuperaRecord(
  arg: "libro" | "rivista"arg: keyof RegistroTipiDati & string,
  id: stringid: string,
) {}

// @filename: data/libro.ts
export class class LibroLibro {
  Libro.numeroDeweyDecimale(): numbernumeroDeweyDecimale(): number {
    return 42;
  }
}
declare module "../lib/registro" {
  export interface RegistroTipiDati {
    RegistroTipiDati.libro: Librolibro: class LibroLibro;
  }
}

// @filename: data/rivista.ts
export class class RivistaRivista {
  Rivista.numeroFascicolo(): numbernumeroFascicolo(): number {
    return 42;
  }
}
declare module "../lib/registro" {
  export interface RegistroTipiDati {
    RegistroTipiDati.rivista: Rivistarivista: class RivistaRivista;
  }
}

// @filename: index.ts
import "./data/libro";
import "./data/rivista";
import { function recuperaRecord(arg: keyof RegistroTipiDati & string, id: string): voidrecuperaRecord } from "./lib/registro";

function recuperaRecord(arg: keyof RegistroTipiDati & string, id: string): voidrecuperaRecord("
  • libro
  • rivista
libro", "lb_123");

Ovviamente ci sarebbero altre cose da costruire per completare una funzione recuperaRecord vera e propria. Non preoccuparti! Ci torneremo una volta che avremo imparato qualche altra cosa che ci serve!

Footnotes

  1. Se sei curioso riguardo a questa proprietà, prova a eseguire questo comando nel tuo terminale: node -e "console.log(new Date()[Symbol.toPrimitive]('string'))". ↩