Esplora i tipi di template literal di TypeScript e crea un motore di validazione in runtime per una verifica robusta delle stringhe e sicurezza dei tipi.
TypeScript Template Literal Validation Engine: Runtime String Verification
I tipi di template literal di TypeScript offrono potenti manipolazioni di stringhe e sicurezza dei tipi a tempo di compilazione. Tuttavia, questi controlli sono limitati al tempo di compilazione. Questo post del blog esplora come costruire un motore di validazione in runtime per i tipi di template literal di TypeScript, abilitando una verifica robusta delle stringhe e prevenendo potenziali errori durante l'esecuzione del programma.
Introduzione ai Tipi di Template Literal di TypeScript
I tipi di template literal consentono di definire forme di stringa specifiche basate su valori letterali, unioni e inferenza di tipo. Questo abilita un controllo dei tipi preciso e l'autocompletamento, particolarmente utile quando si tratta di dati strutturati o linguaggi specifici del dominio.
Ad esempio, considera un tipo per rappresentare codici valuta:
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const validCurrency: FormattedCurrencyString = "USD-100"; // OK
const invalidCurrency: FormattedCurrencyString = "CAD-50"; // Errore di tipo a tempo di compilazione
Questo esempio dimostra come TypeScript impone il tipo FormattedCurrencyString a tempo di compilazione. Tuttavia, se il codice valuta proviene da una fonte esterna (ad esempio, input dell'utente, risposta API), è necessaria una validazione in runtime per garantire la sicurezza dei tipi.
La Necessità della Validazione in Runtime
Mentre TypeScript fornisce un eccellente controllo dei tipi a tempo di compilazione, non può garantire la validità dei dati ricevuti da fonti esterne a tempo di esecuzione. Affidarsi esclusivamente ai tipi di compilazione può portare a errori imprevisti e vulnerabilità.
Considera il seguente scenario:
function processCurrency(currencyString: FormattedCurrencyString) {
// ... qualche logica che presuppone che la stringa sia formattata correttamente
}
const userInput = "CAD-50"; // Supponiamo che provenga dall'input dell'utente
// Questo compilerà, ma causerà un errore di runtime se la logica all'interno
// di `processCurrency` si basa sul formato.
processCurrency(userInput as FormattedCurrencyString);
In questo caso, stiamo effettuando un cast di userInput a FormattedCurrencyString, bypassando i controlli a tempo di compilazione di TypeScript. Se processCurrency si basa sul fatto che la stringa sia formattata correttamente, incontrerà un errore di runtime.
La validazione in runtime colma questa lacuna verificando che i dati ricevuti a tempo di esecuzione siano conformi ai tipi TypeScript attesi.
Costruire un Motore di Validazione per Template Literal
Possiamo costruire un motore di validazione in runtime utilizzando espressioni regolari e il sistema di tipi di TypeScript. Il motore prenderà in input un tipo di template literal e una stringa e restituirà se la stringa corrisponde al tipo.
Passaggio 1: Definire un Tipo per la Validazione in Runtime
Innanzitutto, abbiamo bisogno di un tipo generico che possa rappresentare l'equivalente in runtime di un tipo di template literal. Questo tipo dovrebbe essere in grado di gestire diversi tipi di template literal, inclusi letterali, unioni e parametri di tipo.
type TemplateLiteralToRegex<T extends string> =
T extends `${infer Start}${infer Middle}${infer End}`
? Start extends string
? Middle extends string
? End extends string
? TemplateLiteralToRegexStart<Start> & TemplateLiteralToRegexMiddle<Middle> & TemplateLiteralToRegex<End>
: never
: never
: never
: TemplateLiteralToRegexStart<T>;
type TemplateLiteralToRegexStart<T extends string> = T extends `${infer Literal}` ? Literal : string;
type TemplateLiteralToRegexMiddle<T extends string> = T extends `${infer Literal}` ? Literal : string;
Questa definizione di tipo ricorsiva scompone il template literal nelle sue parti costituenti e converte ogni parte in un pattern di espressione regolare.
Passaggio 2: Implementare la Funzione di Validazione
Successivamente, implementiamo la funzione di validazione che prende in input il tipo di template literal e la stringa da validare. Questa funzione utilizza l'espressione regolare generata da TemplateLiteralToRegex per testare la stringa.
function isValid<T extends string>(str: string, templateType: T): boolean {
const regexPattern = `^${convertTemplateLiteralToRegex(templateType)}$`;
const regex = new RegExp(regexPattern);
return regex.test(str);
}
function convertTemplateLiteralToRegex<T extends string>(templateType: T): string {
// Conversione di base per stringhe letterali - estendere questo per scenari più complessi
return templateType.replace(/[.*+?^${()}|\[\]]/g, '\$&'); // Caratteri speciali regex di escape
}
Questa funzione effettua l'escape dei caratteri speciali delle espressioni regolari e crea un'espressione regolare dal tipo di template literal, quindi testa la stringa rispetto a tale espressione regolare.
Passaggio 3: Utilizzo del Motore di Validazione
Ora puoi utilizzare la funzione isValid per validare le stringhe rispetto ai tuoi tipi di template literal a tempo di esecuzione.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const userInput1 = "USD-100";
const userInput2 = "CAD-50";
console.log(`'${userInput1}' is valid: ${isValid(userInput1, "USD-100" )}`); // true
console.log(`'${userInput2}' is valid: ${isValid(userInput2, "USD-100")}`); // false
console.log(`'${userInput1}' is valid: ${isValid(userInput1, `USD-${100}`)}`); // true
console.log(`'${userInput2}' is valid: ${isValid(userInput2, `USD-${100}`)}`); // false
Questo esempio dimostra come utilizzare la funzione isValid per validare l'input dell'utente rispetto al tipo FormattedCurrencyString. L'output mostrerà se le stringhe di input sono considerate valide o meno, in base al template literal specificato.
Scenari di Validazione Avanzati
Il motore di validazione di base può essere esteso per gestire scenari più complessi, come unioni, tipi condizionali e tipi ricorsivi.
Gestione delle Unioni
Per gestire le unioni, puoi modificare il tipo TemplateLiteralToRegex per generare un'espressione regolare che corrisponda a uno qualsiasi dei membri dell'unione.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
function isValidCurrencyCode<T extends string>(str: string, templateType: T): boolean {
const currencyCodes: CurrencyCode[] = ["USD", "EUR", "GBP"];
return currencyCodes.includes(str as CurrencyCode);
}
function isValidUnionFormattedCurrencyString(str: string): boolean {
const parts = str.split('-');
if(parts.length !== 2) return false;
const [currencyCode, amount] = parts;
if (!isValidCurrencyCode(currencyCode, currencyCode)) return false;
if (isNaN(Number(amount))) return false;
return true;
}
console.log(`'USD-100' is valid formatted string: ${isValidUnionFormattedCurrencyString('USD-100')}`);
console.log(`'CAD-50' is valid formatted string: ${isValidUnionFormattedCurrencyString('CAD-50')}`);
Gestione dei Tipi Condizionali
I tipi condizionali possono essere gestiti valutando la condizione a tempo di esecuzione e generando diverse espressioni regolari in base al risultato.
type IsString<T> = T extends string ? true : false;
// Questo esempio richiede una logica più avanzata e non è completamente implementabile utilizzando semplici regex.
// Le guardie di tipo in runtime offrono una soluzione più robusta in questo scenario specifico.
// Il codice seguente è illustrativo e richiederebbe adattamenti per gestire tipi condizionali complessi.
function isString(value: any): value is string {
return typeof value === 'string';
}
function isValidConditionalType(value: any): boolean {
return isString(value);
}
console.log(`'hello' is a string: ${isValidConditionalType('hello')}`);
console.log(`123 is a string: ${isValidConditionalType(123)}`);
Gestione dei Tipi Ricorsivi
I tipi ricorsivi possono essere gestiti definendo una funzione ricorsiva che genera il pattern dell'espressione regolare. Tuttavia, fai attenzione a evitare ricorsioni infinite ed errori di stack overflow. Per ricorsioni profonde, gli approcci iterativi con limiti appropriati sono cruciali.
Alternative alle Espressioni Regolari
Sebbene le espressioni regolari siano uno strumento potente per la validazione delle stringhe, possono essere complesse e difficili da mantenere. Altri approcci alla validazione in runtime includono:
- Funzioni di Validazione Personalizzate: Scrivi funzioni personalizzate per validare tipi specifici in base ai requisiti della tua applicazione.
- Type Guards: Utilizza le type guards per restringere il tipo di una variabile a tempo di esecuzione.
- Librerie di Validazione: Sfrutta librerie di validazione esistenti come Zod o Yup per semplificare il processo di validazione.
Zod, ad esempio, fornisce una dichiarazione basata su schema che si traduce in un runtime di validazione:
import { z } from 'zod';
const CurrencyCodeSchema = z.enum(['USD', 'EUR', 'GBP']);
const FormattedCurrencyStringSchema = z.string().regex(new RegExp(`^${CurrencyCodeSchema.enum.USD}|${CurrencyCodeSchema.enum.EUR}|${CurrencyCodeSchema.enum.GBP}-[0-9]+$`));
try {
const validCurrency = FormattedCurrencyStringSchema.parse("USD-100");
console.log("Valid Currency:", validCurrency);
} catch (error) {
console.error("Invalid Currency:", error);
}
try {
const invalidCurrency = FormattedCurrencyStringSchema.parse("CAD-50");
console.log("Valid Currency:", invalidCurrency); // Questo non verrà eseguito se parse fallisce.
} catch (error) {
console.error("Invalid Currency:", error);
}
Best Practice per la Validazione in Runtime
Quando implementi la validazione in runtime, tieni a mente le seguenti best practice:
- Valida al Confine: Valida i dati non appena entrano nel tuo sistema (ad esempio, input dell'utente, risposte API).
- Fornisci Messaggi di Errore Chiari: Genera messaggi di errore informativi per aiutare gli utenti a capire perché il loro input non è valido.
- Usa una Strategia di Validazione Coerente: Adotta una strategia di validazione coerente in tutta la tua applicazione per garantire l'integrità dei dati.
- Testa la Tua Logica di Validazione: Testa accuratamente la tua logica di validazione per assicurarti che identifichi correttamente dati validi e non validi.
- Bilancia Prestazioni e Sicurezza: Ottimizza la tua logica di validazione per le prestazioni garantendo al contempo che prevenga efficacemente le vulnerabilità di sicurezza. Evita regex eccessivamente complesse che portano a negazione del servizio.
Considerazioni sull'Internazionalizzazione
Quando si tratta di validazione delle stringhe in un contesto globale, è necessario considerare l'internazionalizzazione (i18n) e la localizzazione (l10n). Diverse localizzazioni possono avere regole diverse per la formattazione delle stringhe, come date, numeri e valori di valuta.
Ad esempio, il simbolo di valuta per l'Euro (€) può apparire prima o dopo l'importo, a seconda della localizzazione. Allo stesso modo, il separatore decimale può essere un punto (.) o una virgola (,).
Per gestire queste variazioni, puoi utilizzare librerie di internazionalizzazione come Intl, che fornisce API per la formattazione e il parsing di dati sensibili alla localizzazione. Ad esempio, potresti adattare l'esempio precedente per gestire diversi formati di valuta:
function isValidCurrencyString(currencyString: string, locale: string): boolean {
try {
const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: currencyString.substring(0,3) }); // Esempio molto basilare
// Tenta di analizzare la valuta usando il formatter. Questo esempio è intenzionalmente molto semplice.
return true;
} catch (error) {
return false;
}
}
console.log(`USD-100 is valid for en-US: ${isValidCurrencyString('USD-100', 'en-US')}`);
console.log(`EUR-100 is valid for fr-FR: ${isValidCurrencyString('EUR-100', 'fr-FR')}`);
Questo snippet di codice fornisce un esempio di base. La corretta internazionalizzazione richiede una gestione più approfondita, potenzialmente l'utilizzo di librerie esterne o API specificamente progettate per la formattazione e la validazione delle valute tra diverse localizzazioni.
Conclusione
La validazione in runtime è una parte essenziale della creazione di applicazioni TypeScript robuste e affidabili. Combinando i tipi di template literal di TypeScript con espressioni regolari o metodi di validazione alternativi, è possibile creare un potente motore per verificare la validità delle stringhe a tempo di esecuzione.
Questo approccio migliora la sicurezza dei tipi, previene errori imprevisti e migliora la qualità generale del tuo codice. Man mano che costruisci applicazioni più complesse, considera l'incorporazione della validazione in runtime per garantire che i tuoi dati siano conformi ai tipi e ai formati previsti.
Ulteriori Esplorazioni
- Esplora tecniche avanzate di espressioni regolari per scenari di validazione più complessi.
- Indaga su librerie di validazione come Zod e Yup per la validazione basata su schema.
- Considera l'utilizzo di tecniche di generazione di codice per generare automaticamente funzioni di validazione da tipi TypeScript.
- Studia librerie e API di internazionalizzazione per gestire dati sensibili alla localizzazione.