Entdecken Sie TypeScript Template-Literal-Typen und erstellen Sie eine Laufzeit-Validierungs-Engine für robuste String-Überprüfung und Typsicherheit. Lernen Sie, Fehler zu vermeiden, indem Sie Strings zur Laufzeit gegen Ihre definierten Template-Literal-Typen validieren.
TypeScript Template-Literal-Validierungs-Engine: Laufzeit-String-Überprüfung
Die Template-Literal-Typen von TypeScript bieten eine leistungsstarke String-Manipulation und Typsicherheit zur Kompilierungszeit. Diese Überprüfungen sind jedoch auf die Kompilierungszeit beschränkt. Dieser Blogbeitrag untersucht, wie man eine Laufzeit-Validierungs-Engine für TypeScript-Template-Literal-Typen erstellt, die eine robuste String-Überprüfung ermöglicht und potenzielle Fehler während der Programmausführung verhindert.
Einführung in TypeScript Template-Literal-Typen
Template-Literal-Typen ermöglichen es Ihnen, spezifische String-Formen basierend auf Literalwerten, Unions und Typinferenz zu definieren. Dies ermöglicht eine präzise Typüberprüfung und Autovervollständigung, was besonders nützlich ist, wenn man mit strukturierten Daten oder domänenspezifischen Sprachen arbeitet.
Betrachten Sie zum Beispiel einen Typ zur Darstellung von Währungscodes:
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const validCurrency: FormattedCurrencyString = "USD-100"; // OK
const invalidCurrency: FormattedCurrencyString = "CAD-50"; // Typfehler zur Kompilierungszeit
Dieses Beispiel zeigt, wie TypeScript den FormattedCurrencyString-Typ zur Kompilierungszeit erzwingt. Wenn der Währungscode jedoch aus einer externen Quelle stammt (z. B. Benutzereingabe, API-Antwort), benötigen Sie eine Laufzeitvalidierung, um die Typsicherheit zu gewährleisten.
Die Notwendigkeit der Laufzeitvalidierung
Obwohl TypeScript eine ausgezeichnete Typüberprüfung zur Kompilierungszeit bietet, kann es die Gültigkeit von Daten, die zur Laufzeit aus externen Quellen empfangen werden, nicht garantieren. Sich ausschließlich auf Typen zur Kompilierungszeit zu verlassen, kann zu unerwarteten Fehlern und Schwachstellen führen.
Betrachten Sie das folgende Szenario:
function processCurrency(currencyString: FormattedCurrencyString) {
// ... eine Logik, die davon ausgeht, dass der String korrekt formatiert ist
}
const userInput = "CAD-50"; // Angenommen, dies kommt von einer Benutzereingabe
// Dies wird kompiliert, verursacht aber einen Laufzeitfehler, wenn die Logik innerhalb
// von `processCurrency` auf dem Format beruht.
processCurrency(userInput as FormattedCurrencyString);
In diesem Fall casten wir userInput zu FormattedCurrencyString und umgehen damit die Prüfungen von TypeScript zur Kompilierungszeit. Wenn processCurrency darauf angewiesen ist, dass der String korrekt formatiert ist, wird ein Laufzeitfehler auftreten.
Die Laufzeitvalidierung schließt diese Lücke, indem sie überprüft, ob die zur Laufzeit empfangenen Daten den erwarteten TypeScript-Typen entsprechen.
Aufbau einer Template-Literal-Validierungs-Engine
Wir können eine Laufzeit-Validierungs-Engine mit regulären Ausdrücken und dem Typsystem von TypeScript erstellen. Die Engine nimmt einen Template-Literal-Typ und einen String als Eingabe entgegen und gibt zurück, ob der String dem Typ entspricht.
Schritt 1: Definieren eines Typs für die Laufzeitvalidierung
Zuerst benötigen wir einen generischen Typ, der das Laufzeitäquivalent eines Template-Literal-Typs darstellen kann. Dieser Typ sollte verschiedene Arten von Template-Literalen handhaben können, einschließlich Literalen, Unions und Typparametern.
type TemplateLiteralToRegex =
T extends `${infer Start}${infer Middle}${infer End}`
? Start extends string
? Middle extends string
? End extends string
? TemplateLiteralToRegexStart & TemplateLiteralToRegexMiddle & TemplateLiteralToRegex
: never
: never
: never
: TemplateLiteralToRegexStart;
type TemplateLiteralToRegexStart = T extends `${infer Literal}` ? Literal : string;
type TemplateLiteralToRegexMiddle = T extends `${infer Literal}` ? Literal : string;
Diese rekursive Typdefinition zerlegt das Template-Literal in seine Bestandteile und wandelt jeden Teil in ein reguläres Ausdrucksmuster um.
Schritt 2: Implementierung der Validierungsfunktion
Als Nächstes implementieren wir die Validierungsfunktion, die den Template-Literal-Typ und den zu validierenden String als Eingabe entgegennimmt. Diese Funktion verwendet den von TemplateLiteralToRegex generierten regulären Ausdruck, um den String zu testen.
function isValid(str: string, templateType: T): boolean {
const regexPattern = `^${convertTemplateLiteralToRegex(templateType)}$`;
const regex = new RegExp(regexPattern);
return regex.test(str);
}
function convertTemplateLiteralToRegex(templateType: T): string {
// Grundlegende Konvertierung für literale Strings - erweitern Sie dies für komplexere Szenarien
return templateType.replace(/[.*+?^${}()|[\]]/g, '\\$&'); // Sonderzeichen für Regex escapen
}
Diese Funktion escaped spezielle Zeichen für reguläre Ausdrücke und erstellt einen regulären Ausdruck aus dem Template-Literal-Typ, um dann den String damit zu testen.
Schritt 3: Verwendung der Validierungs-Engine
Jetzt können Sie die isValid-Funktion verwenden, um Strings zur Laufzeit gegen Ihre Template-Literal-Typen zu validieren.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const userInput1 = "USD-100";
const userInput2 = "CAD-50";
console.log(`'${userInput1}' ist gültig: ${isValid(userInput1, "USD-100" )}`); // true
console.log(`'${userInput2}' ist gültig: ${isValid(userInput2, "USD-100")}`); // false
console.log(`'${userInput1}' ist gültig: ${isValid(userInput1, `USD-${100}`)}`); // true
console.log(`'${userInput2}' ist gültig: ${isValid(userInput2, `USD-${100}`)}`); // false
Dieses Beispiel zeigt, wie man die isValid-Funktion verwendet, um Benutzereingaben gegen den FormattedCurrencyString-Typ zu validieren. Die Ausgabe zeigt an, ob die Eingabe-Strings als gültig oder ungültig betrachtet werden, basierend auf dem angegebenen Template-Literal.
Fortgeschrittene Validierungsszenarien
Die grundlegende Validierungs-Engine kann erweitert werden, um komplexere Szenarien wie Unions, bedingte Typen und rekursive Typen zu handhaben.
Umgang mit Unions
Um Unions zu handhaben, können Sie den TemplateLiteralToRegex-Typ ändern, um einen regulären Ausdruck zu generieren, der auf jedes der Union-Mitglieder passt.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
function isValidCurrencyCode(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' ist ein gültiger formatierter String: ${isValidUnionFormattedCurrencyString('USD-100')}`);
console.log(`'CAD-50' ist ein gültiger formatierter String: ${isValidUnionFormattedCurrencyString('CAD-50')}`);
Umgang mit bedingten Typen (Conditional Types)
Bedingte Typen können gehandhabt werden, indem die Bedingung zur Laufzeit ausgewertet und je nach Ergebnis unterschiedliche reguläre Ausdrücke generiert werden.
type IsString = T extends string ? true : false;
// Dieses Beispiel erfordert fortgeschrittenere Logik und ist nicht vollständig mit einfachen Regex implementierbar.
// Laufzeit-Type-Guards bieten in diesem speziellen Szenario eine robustere Lösung.
// Der folgende Code ist illustrativ und müsste angepasst werden, um komplexe bedingte Typen zu handhaben.
function isString(value: any): value is string {
return typeof value === 'string';
}
function isValidConditionalType(value: any): boolean {
return isString(value);
}
console.log(`'hello' ist ein String: ${isValidConditionalType('hello')}`);
console.log(`123 ist ein String: ${isValidConditionalType(123)}`);
Umgang mit rekursiven Typen
Rekursive Typen können durch die Definition einer rekursiven Funktion gehandhabt werden, die das Muster des regulären Ausdrucks generiert. Seien Sie jedoch vorsichtig, um unendliche Rekursion und Stack-Overflow-Fehler zu vermeiden. Bei tiefer Rekursion sind iterative Ansätze mit entsprechenden Grenzen entscheidend.
Alternativen zu regulären Ausdrücken
Obwohl reguläre Ausdrücke ein mächtiges Werkzeug für die String-Validierung sind, können sie komplex und schwer zu warten sein. Andere Ansätze zur Laufzeitvalidierung umfassen:
- Benutzerdefinierte Validierungsfunktionen: Schreiben Sie benutzerdefinierte Funktionen, um spezifische Typen basierend auf den Anforderungen Ihrer Anwendung zu validieren.
- Type Guards: Verwenden Sie Type Guards, um den Typ einer Variable zur Laufzeit einzugrenzen.
- Validierungsbibliotheken: Nutzen Sie bestehende Validierungsbibliotheken wie Zod oder Yup, um den Validierungsprozess zu vereinfachen.
Zod zum Beispiel bietet eine schemabasierte Deklaration, die in eine Laufzeitvalidierung übersetzt wird:
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("Gültige Währung:", validCurrency);
} catch (error) {
console.error("Ungültige Währung:", error);
}
try {
const invalidCurrency = FormattedCurrencyStringSchema.parse("CAD-50");
console.log("Gültige Währung:", invalidCurrency); // Dies wird nicht ausgeführt, wenn `parse` fehlschlägt.
} catch (error) {
console.error("Ungültige Währung:", error);
}
Best Practices für die Laufzeitvalidierung
Beachten Sie bei der Implementierung der Laufzeitvalidierung die folgenden Best Practices:
- Validieren an der Systemgrenze: Validieren Sie Daten, sobald sie in Ihr System gelangen (z. B. Benutzereingaben, API-Antworten).
- Bereitstellen klarer Fehlermeldungen: Generieren Sie informative Fehlermeldungen, damit Benutzer verstehen, warum ihre Eingabe ungültig ist.
- Verwenden Sie eine konsistente Validierungsstrategie: Übernehmen Sie eine konsistente Validierungsstrategie in Ihrer gesamten Anwendung, um die Datenintegrität zu gewährleisten.
- Testen Sie Ihre Validierungslogik: Testen Sie Ihre Validierungslogik gründlich, um sicherzustellen, dass sie gültige und ungültige Daten korrekt identifiziert.
- Leistung und Sicherheit abwägen: Optimieren Sie Ihre Validierungslogik im Hinblick auf die Leistung und stellen Sie gleichzeitig sicher, dass sie Sicherheitslücken wirksam verhindert. Vermeiden Sie übermäßig komplexe Regex, die zu Denial-of-Service-Angriffen führen können.
Überlegungen zur Internationalisierung
Wenn Sie mit der String-Validierung in einem globalen Kontext arbeiten, müssen Sie die Internationalisierung (i18n) und Lokalisierung (l10n) berücksichtigen. Unterschiedliche Gebietsschemata können unterschiedliche Regeln für die Formatierung von Strings haben, wie z. B. Daten, Zahlen und Währungswerte.
Zum Beispiel kann das Währungssymbol für den Euro (€) je nach Gebietsschema vor oder nach dem Betrag erscheinen. Ebenso kann das Dezimaltrennzeichen ein Punkt (.) oder ein Komma (,) sein.
Um diese Variationen zu handhaben, können Sie Internationalisierungsbibliotheken wie Intl verwenden, die APIs zur Formatierung und zum Parsen von gebietsschemasensitiven Daten bereitstellen. Sie könnten beispielsweise das vorherige Beispiel anpassen, um verschiedene Währungsformate zu handhaben:
function isValidCurrencyString(currencyString: string, locale: string): boolean {
try {
const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: currencyString.substring(0,3) }); // Sehr einfaches Beispiel
// Versuch, die Währung mit dem Formatter zu parsen. Dieses Beispiel ist absichtlich sehr einfach gehalten.
return true;
} catch (error) {
return false;
}
}
console.log(`USD-100 ist gültig für en-US: ${isValidCurrencyString('USD-100', 'en-US')}`);
console.log(`EUR-100 ist gültig für fr-FR: ${isValidCurrencyString('EUR-100', 'fr-FR')}`);
Dieser Codeausschnitt bietet ein grundlegendes Beispiel. Eine ordnungsgemäße Internationalisierung erfordert eine gründlichere Handhabung, möglicherweise unter Verwendung externer Bibliotheken oder APIs, die speziell für die Währungsformatierung und -validierung in verschiedenen Gebietsschemata entwickelt wurden.
Fazit
Die Laufzeitvalidierung ist ein wesentlicher Bestandteil beim Erstellen robuster und zuverlässiger TypeScript-Anwendungen. Durch die Kombination von TypeScript-Template-Literal-Typen mit regulären Ausdrücken oder alternativen Validierungsmethoden können Sie eine leistungsstarke Engine zur Überprüfung der Gültigkeit von Strings zur Laufzeit erstellen.
Dieser Ansatz erhöht die Typsicherheit, verhindert unerwartete Fehler und verbessert die Gesamtqualität Ihres Codes. Wenn Sie komplexere Anwendungen erstellen, sollten Sie die Einbeziehung der Laufzeitvalidierung in Betracht ziehen, um sicherzustellen, dass Ihre Daten den erwarteten Typen und Formaten entsprechen.
Weiterführende Themen
- Erkunden Sie fortgeschrittene Techniken für reguläre Ausdrücke für komplexere Validierungsszenarien.
- Untersuchen Sie Validierungsbibliotheken wie Zod und Yup für schemabasierte Validierung.
- Erwägen Sie die Verwendung von Codegenerierungstechniken, um Validierungsfunktionen automatisch aus TypeScript-Typen zu generieren.
- Studieren Sie Internationalisierungsbibliotheken und APIs zur Handhabung von gebietsschemasensitiven Daten.