Record<K, V> è un utility type integrato per un oggetto le cui chiavi hanno tipo K e i cui valori hanno tutti tipo V. Con chiavi string descrive un dizionario; con un'unione di chiavi letterali descrive un oggetto che deve avere esattamente quelle chiavi.
Record esiste solo nel sistema dei tipi. In fase di esecuzione entrambi gli oggetti sono normali oggetti JavaScript, quindi funzionano con oggetti letterali, spread, JSON.stringify e tutto ciò che accetta un oggetto.
Sintassi e definizione
Record<Keys, Value>
Keys deve essere qualcosa che può fare da chiave di un oggetto: string, number, symbol, un'unione di letterali stringa o numerici, o un template literal type. Value può essere qualsiasi tipo. L'intera definizione nella libreria standard di TypeScript è una riga, un mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any è string | number | symbol, l'insieme di tutti i tipi di chiave possibili. [P in K]: T crea una proprietà di tipo T per ogni membro di K. Questo spiega entrambi i comportamenti descritti qui sotto: un K ampio come string produce una index signature (qualsiasi chiave), e un K unione produce una proprietà obbligatoria per ogni membro.
Chiavi in unione: ogni chiave è obbligatoria
Quando le chiavi sono un'unione di letterali, un Record deve elencarle tutte, e nessun'altra. Così il compilatore diventa una lista di controllo:
Aggiungi "cancelled" a Status ed entrambi gli oggetti smettono di compilare finché non dai al nuovo stato un'etichetta e un colore. Omettere una chiave, o aggiungerne una che non è nell'unione, è un errore di compilazione:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
Lo stesso funziona con un enum di stringhe come tipo delle chiavi: Record<Color, string> richiede una voce per ogni membro dell'enum.
Record<string, T> e la chiave mancante
Con chiavi string, qualsiasi chiave è ammessa, e TypeScript tipizza ogni lettura come T, anche per una chiave che non c'è. In fase di esecuzione una chiave mancante dà undefined:
È il bug più comune con Record. Ci sono tre modi per gestirlo: controllare con in o Object.hasOwn prima di leggere, dichiarare il valore come V | undefined, oppure attivare l'opzione del compilatore noUncheckedIndexedAccess, che aggiunge | undefined a ogni lettura tramite index signature nel progetto. Un Record con chiavi in unione non ha questo problema, perché ogni chiave esiste di sicuro.
Partial<Record<K, V>>: solo alcune chiavi
Per usare un'unione di chiavi senza richiederle tutte, avvolgi il Record in Partial. Le letture restituiscono allora V | undefined, il che è onesto:
Una chiave scritta male come jp resta un errore, ed è questo il vantaggio rispetto a Record<string, string>.
Iterare su un Record
Object.keys, Object.values e Object.entries funzionano tutti. Il problema è il tipo delle chiavi: Object.keys restituisce string[] e Object.entries restituisce [string, V][], mai la tua unione di chiavi:
TypeScript tiene le chiavi come string di proposito: in fase di esecuzione un oggetto può avere più proprietà di quelle elencate dal suo tipo, quindi promettere Plan[] non sarebbe sicuro in generale. Per un oggetto che hai creato da un letterale, come seats, il cast è sicuro.
Costruire un Record dai dati
I Record sono il risultato abituale quando raggruppi o indicizzi un array. Parti da un oggetto vuoto con il tipo Record e riempilo:
Book["genre"] riutilizza l'unione dell'interfaccia come tipo delle chiavi, quindi aggiungere un genere a Book fa sì che byGenre richieda una nuova voce.
Record<string, unknown> e le interfacce
Record<string, unknown> è un tipo comune per "un oggetto qualsiasi con chiavi stringa". Accetta oggetti letterali e valori tipizzati con un alias type, ma un'interfaccia viene rifiutata:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
Le interfacce si possono estendere tramite la fusione delle dichiarazioni, quindi TypeScript non presume che siano compatibili con una index signature; gli alias di tipo non si possono riaprire, quindi un type User = { name: string } passerebbe. Le soluzioni abituali sono accettare object al suo posto (puoi comunque chiamare Object.keys su di esso), rendere la funzione generica (<T extends object>(obj: T)), oppure dichiarare User con type (la pagina interface vs type tratta le altre differenze).
Record vs index signature vs Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| Esiste in esecuzione | no, un oggetto normale | no, un oggetto normale | sì, una classe |
| Insieme fisso di chiavi | sì, con un K unione | no | no |
| Tipi di chiave | string, number, symbol, unioni di letterali, template pattern | string, number, symbol, template pattern | qualsiasi, oggetti compresi |
| Tipo della lettura di una chiave mancante | V (con chiavi string) | V | V | undefined da get |
| Insieme a proprietà con nome | tramite intersezione & | sì, nello stesso tipo | no |
| JSON e spread | sì | sì | no, prima va convertita |
| Dimensione | Object.keys(r).length | Object.keys(o).length | m.size |
| Aggiunte e rimozioni frequenti | funziona | funziona | pensata per questo |
Scegli Record con chiavi in unione ogni volta che l'insieme delle chiavi è noto: è l'unica opzione che controlla che ogni chiave sia presente. Per chiavi stringa aperte, Record<string, V> e una index signature sono intercambiabili, e molti codebase preferiscono Record per leggibilità. Ricorri a una Map quando le chiavi vengono aggiunte e rimosse in fase di esecuzione, quando le chiavi non sono stringhe, o quando ti servono la dimensione e l'ordine di inserimento senza lavoro in più.
Domande frequenti
Cos'è Record in TypeScript?
Record<K, V> è un utility type integrato per un oggetto le cui chiavi sono di tipo K e i cui valori sono tutti di tipo V. Record<string, number> è un oggetto con qualsiasi chiave string e valori number; Record<"en" | "de", string> è un oggetto con esattamente le chiavi en e de, entrambe string.
Che differenza c'è tra Record e Map in TypeScript?
Record è un tipo per un normale oggetto JavaScript, quindi funziona con oggetti letterali, JSON e spread, e sparisce in compilazione. Map è una classe che esiste in fase di esecuzione con get, set, has e size, mantiene l'ordine di inserimento di tutte le chiavi, accetta chiavi di qualsiasi tipo (anche oggetti), e get restituisce V | undefined. Usa un Record per dati fissi o con la forma di JSON e una Map per chiavi aggiunte e rimosse in esecuzione.
Che differenza c'è tra Record<string, T> e { [key: string]: T }?
Per i valori sono lo stesso tipo: Record<string, T> si espande in un tipo oggetto con una index signature di tipo string. Ci sono due piccole differenze: una index signature può avere un nome e stare accanto ad altre proprietà nello stesso tipo, e keyof Record<string, T> è string mentre keyof { [key: string]: T } è string | number.
Come itero su un Record in TypeScript?
Usa Object.entries(record) per le coppie chiave-valore, Object.keys per le chiavi e Object.values per i valori. Le chiavi arrivano come string, non come K, perché un oggetto può avere chiavi in più in fase di esecuzione. Quando il Record ha un'unione di chiavi note, fai un cast: (Object.keys(r) as Array<keyof typeof r>).
Come rendo obbligatorie solo alcune chiavi di un Record?
Con chiavi in unione, Record<K, V> richiede tutte le chiavi. Avvolgilo in Partial per renderle tutte opzionali: Partial<Record<Lang, string>>. Per un misto, usa un'intersezione: Record<"en", string> & Partial<Record<"de" | "fr", string>> richiede en e permette le altre.