value satisfies Type controlla in fase di compilazione che value corrisponda a Type, e poi lascia stare il tipo proprio del valore, più preciso. Un'annotazione sostituirebbe quel tipo preciso con Type; satisfies convalida senza allargare.
satisfies fa comunque il controllo: un colore mancante, una chiave scritta male come bleu o un valore come true è un errore di compilazione su quella riga. Esiste da TypeScript 4.9 e, come ogni annotazione di tipo, viene rimosso dal JavaScript generato.
Il problema che satisfies risolve
Con un'annotazione di tipo, il tipo della variabile è l'annotazione. Il compilatore dimentica ciò che ha visto nel letterale. Qui la stessa tavolozza è annotata, e ora TypeScript non sa più che green è una stringa:
Il compilatore segnala:
index.ts(11,27): error TS2339: Property 'toUpperCase' does not exist on type 'Color'.
Property 'toUpperCase' does not exist on type '[number, number, number]'.
Prima di TypeScript 4.9 le scelte erano: annotare e restringere a mano ovunque (typeof palette.green === "string"), oppure saltare l'annotazione e perdere il controllo. satisfies dà entrambe le cose. Cambia : Record<ColorName, Color> in satisfies Record<ColorName, Color> dopo la parentesi graffa di chiusura e funziona.
satisfies vs annotazione di tipo vs as
Lo stesso oggetto di impostazioni, scritto in tre modi:
as ha lasciato passare il lang mancante, e asserted.lang è undefined in fase di esecuzione mentre il suo tipo dice string. Togli lang dalle altre due righe ed entrambe falliscono con TS2741, Property 'lang' is missing in type ....
Annotazione const x: T = v | Asserzione v as T | v satisfies T | |
|---|---|---|---|
| Proprietà mancanti | errore | ammesse | errore |
| Proprietà in eccesso (oggetto letterale) | errore | ammesse | errore |
| Tipo di proprietà sbagliato | errore | solo se i tipi non si sovrappongono | errore |
Tipo di x dopo | T | T | il tipo inferito di v |
Tipi letterali ("dark", 8080) | allargati a T | allargati a T | mantenuti dove T li ammette |
Chiavi di un Record<string, ...> | qualsiasi stringa (gli errori di battitura compilano) | qualsiasi stringa | esattamente le chiavi scritte |
| Effetto in esecuzione | nessuno | nessuno | nessuno |
Regola pratica: annota quando vuoi che la variabile abbia il tipo dichiarato (un valore che riassegnerai, un'API pubblica), e usa satisfies quando vuoi un controllo ma il tipo proprio del valore è più utile.
Intercettare errori negli oggetti letterali
satisfies esegue il controllo di assegnabilità completo, compreso quello delle proprietà in eccesso, quindi gli errori di battitura nelle chiavi sono errori:
type Route = { path: string; method: "GET" | "POST" };
const home = { path: "/", metod: "GET" } satisfies Route;
// error TS2561: Object literal may only specify known properties, but 'metod' does not exist in type 'Route'. Did you mean to write 'method'?
Il controllo dà anche al letterale un tipo contestuale, proprio come fa un'annotazione. Questo conta in due modi. I letterali stringa vengono mantenuti come tipi letterali quando il tipo di destinazione se li aspetta: { path: "/", method: "GET" } satisfies Route ha method: "GET", mentre lo stesso oggetto senza annotazione inferirebbe method: string. E i parametri delle callback vengono inferiti dal tipo di destinazione:
Le chiavi di un Record restano note
Un uso comune è una tabella di lookup. Annotata come Record<string, T>, ogni stringa è una chiave valida e un errore di battitura compila, restituendo undefined in fase di esecuzione. Con satisfies, i valori vengono comunque controllati rispetto a T, ma il tipo della variabile elenca esattamente le chiavi che hai scritto:
keyof typeof endpoints è utile solo perché le chiavi sono sopravvissute. Con l'annotazione sarebbe un semplice string.
Per richiedere un insieme fisso di chiavi, usa satisfies con un Record su un'unione: satisfies Record<"dev" | "prod", string> segnala un prod mancante con TS2741 e uno staging sconosciuto con TS2353.
as const satisfies
as const e satisfies si combinano. Scrivi prima as const: rende il valore readonly in profondità con tipi letterali, poi satisfies controlla quel valore esatto.
Ogni route viene controllata rispetto a Route (un method: "PUT" sarebbe un errore), e la tupla di tipi letterali resta disponibile, quindi Path è un'unione dei percorsi reali. Usa readonly Route[] (o ReadonlyArray<Route>) come destinazione, dato che un array as const è readonly.
Oggetti di configurazione
La configurazione è il campo in cui satisfies si guadagna il posto: la forma deve essere giusta, e il codice altrove vuole i valori precisi.
Dimentica la voce production, scrivi male logLevel o scrivi logLevel: "verbose", e il compilatore indica la riga esatta. Lo stesso schema va bene per i file *.config.ts: export default { ... } satisfies SomeConfig controlla l'intero file mentre l'oggetto esportato mantiene i suoi valori letterali.
Quando non usare satisfies
- La variabile verrà riassegnata.
let cfg = { port: 3000 } satisfies { port: number | string }dà acfgil tipo{ port: number }, quindi un successivocfg = { port: "80" }fallisce (TS2322). Annota le variabili che intendi modificare. - Vuoi di proposito il tipo dichiarato. Per il valore di ritorno di una funzione o una costante esportata che fa parte di un'API, il tipo dell'annotazione è il contratto, e far trapelare il tipo letterale esatto può rendere incompatibili le modifiche future.
- Il valore non è un letterale.
satisfiesdà il meglio su oggetti e array letterali. Su una variabile o sul risultato di una chiamata è un semplice controllo di assegnabilità, che un'annotazione ti dà già.
Domande frequenti
Cosa fa satisfies in TypeScript?
expression satisfies Type controlla in fase di compilazione che l'espressione sia assegnabile a Type, segnalando proprietà mancanti, proprietà in eccesso e tipi di valore sbagliati, e poi lascia invariato il tipo inferito dell'espressione. Ottieni la sicurezza di un'annotazione e la precisione dell'inferenza. Viene cancellato dall'output JavaScript.
Che differenza c'è tra satisfies e un'annotazione di tipo?
Entrambi controllano il valore. Un'annotazione (const x: T = ...) poi dà alla variabile il tipo T, dimenticando ciò che il compilatore sapeva del valore (tipi letterali, quale membro dell'unione è ogni proprietà, quali chiavi esistono). satisfies T mantiene il tipo inferito, quindi si sa che x.someKey esiste e una proprietà string | number che contiene una stringa ha tipo string.
Che differenza c'è tra satisfies e as in TypeScript?
as è un'asserzione: sostituisce il tipo e non controlla quasi nulla, quindi le proprietà mancanti passano inosservate. satisfies è un controllo: il valore deve corrispondere davvero al tipo, e il suo tipo inferito viene mantenuto. Quando compilerebbero entrambi, satisfies è la scelta più sicura.
Cosa significa as const satisfies?
Applica entrambe le cose: as const rende il valore readonly in profondità con tipi letterali, poi satisfies controlla quel risultato rispetto a un tipo. Scrivi prima as const: const routes = [...] as const satisfies readonly Route[];. La variabile mantiene i tipi letterali esatti per usarli dopo, e una voce sbagliata resta un errore di compilazione.
Quale versione di TypeScript ha aggiunto satisfies?
TypeScript 4.9, uscito a novembre 2022. È sintassi cancellabile, quindi funziona anche con il type stripping integrato di Node, e tutte le versioni attuali di TypeScript (compresa la 7) lo supportano.