Explore TypeScript's powerful template literal types for advanced string manipulation, pattern matching, and validation. Learn with practical examples and real-world use cases.
Template Literal Types: String Pattern Matching and Validation in TypeScript
TypeScript's type system is constantly evolving, offering developers more powerful tools to express complex logic and ensure type safety. One of the most interesting and versatile features introduced in recent versions is template literal types. These types allow you to manipulate strings at the type level, enabling advanced string pattern matching and validation. This opens up a whole new world of possibilities for creating more robust and maintainable applications.
What are Template Literal Types?
Template literal types are a form of type that is constructed by combining string literal types and union types, similar to how template literals work in JavaScript. However, instead of creating runtime strings, they create new types based on existing ones.
Here's a basic example:
type Greeting<T extends string> = `Hello, ${T}!`;
type MyGreeting = Greeting<"World">; // type MyGreeting = "Hello, World!"
In this example, `Greeting` is a template literal type that takes a string type `T` as input and returns a new type that is the concatenation of "Hello, ", `T`, and "!".
Basic String Pattern Matching
Template literal types can be used to perform basic string pattern matching. This allows you to create types that are only valid if they match a certain pattern.
For instance, you can create a type that only accepts strings that start with "prefix-":
type PrefixedString<T extends string> = T extends `prefix-${string}` ? T : never;
type ValidPrefixedString = PrefixedString<"prefix-valid">; // type ValidPrefixedString = "prefix-valid"
type InvalidPrefixedString = PrefixedString<"invalid">; // type InvalidPrefixedString = never
In this example, `PrefixedString` uses a conditional type to check if the input string `T` starts with "prefix-". If it does, the type is `T` itself; otherwise, it's `never`. `never` is a special type in TypeScript that represents the type of values that never occur, effectively excluding the invalid string.
Extracting Parts of a String
Template literal types can also be used to extract parts of a string. This is particularly useful when you need to parse data from strings and convert it into different types.
Let's say you have a string that represents a coordinate in the format "x:10,y:20". You can use template literal types to extract the x and y values:
type CoordinateString = `x:${number},y:${number}`;
type ExtractX<T extends CoordinateString> = T extends `x:${infer X},y:${number}` ? X : never;
type ExtractY<T extends CoordinateString> = T extends `x:${number},y:${infer Y}` ? Y : never;
type XValue = ExtractX<"x:10,y:20">; // type XValue = 10
type YValue = ExtractY<"x:10,y:20">; // type YValue = 20
In this example, `ExtractX` and `ExtractY` use the `infer` keyword to capture the parts of the string that match the `number` type. `infer` allows you to extract a type from a pattern match. The captured types are then used as the return type of the conditional type.
Advanced String Validation
Template literal types can be combined with other TypeScript features, such as union types and conditional types, to perform advanced string validation. This allows you to create types that enforce complex rules on the structure and content of strings.
For example, you can create a type that validates ISO 8601 date strings:
type Year = `${number}${number}${number}${number}`;
type Month = `0${number}` | `10` | `11` | `12`;
type Day = `${0}${number}` | `${1 | 2}${number}` | `30` | `31`;
type ISODate = `${Year}-${Month}-${Day}`;
type ValidDate = ISODate extends "2023-10-27" ? true : false; // true
type InvalidDate = ISODate extends "2023-13-27" ? true : false; // false
function processDate(date: ISODate) {
// Function logic here. TypeScript enforces the ISODate format.
return `Processing date: ${date}`;
}
console.log(processDate("2024-01-15")); // Works
//console.log(processDate("2024-1-15")); // TypeScript error: Argument of type '"2024-1-15"' is not assignable to parameter of type '`${number}${number}${number}${number}-${0}${number}-${0}${number}` | `${number}${number}${number}${number}-${0}${number}-${1}${number}` | ... 14 more ... | `${number}${number}${number}${number}-12-31`'.
Here, `Year`, `Month`, and `Day` are defined using template literal types to represent the valid formats for each part of the date. `ISODate` then combines these types to create a type that represents a valid ISO 8601 date string. The example also demonstrates how this type can be used to enforce data formatting in a function, preventing incorrect date formats from being passed. This improves code reliability and prevents runtime errors caused by invalid input.
Real-World Use Cases
Template literal types can be used in a variety of real-world scenarios. Here are a few examples:
- Form Validation: You can use template literal types to validate the format of form inputs, such as email addresses, phone numbers, and postal codes.
- API Request Validation: You can use template literal types to validate the structure of API request payloads, ensuring that they conform to the expected format. For example, validating a currency code (e.g., "USD", "EUR", "GBP").
- Configuration File Parsing: You can use template literal types to parse configuration files and extract values based on specific patterns. Consider validating file paths in a configuration object.
- String-Based Enums: You can create string-based enums with validation using template literal types.
Example: Validating Currency Codes
Let's look at a more detailed example of validating currency codes. We want to ensure that only valid ISO 4217 currency codes are used in our application. These codes are typically three uppercase letters.
type CurrencyCode = `${Uppercase<string>}${Uppercase<string>}${Uppercase<string>}`;
function formatCurrency(amount: number, currency: CurrencyCode) {
// Function logic to format currency based on the provided code.
return `$${amount} ${currency}`;
}
console.log(formatCurrency(100, "USD")); // Works
//console.log(formatCurrency(100, "usd")); // TypeScript error: Argument of type '"usd"' is not assignable to parameter of type '`${Uppercase}${Uppercase}${Uppercase}`'.
//More precise example:
type ValidCurrencyCode = "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD"; // Extend as needed
type StronglyTypedCurrencyCode = ValidCurrencyCode;
function formatCurrencyStronglyTyped(amount: number, currency: StronglyTypedCurrencyCode) {
return `$${amount} ${currency}`;
}
console.log(formatCurrencyStronglyTyped(100, "EUR")); // Works
//console.log(formatCurrencyStronglyTyped(100, "CNY")); // TypeScript error: Argument of type '"CNY"' is not assignable to parameter of type '"USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD"'.
This example demonstrates how to create a `CurrencyCode` type that only accepts strings consisting of three uppercase characters. The second, more strongly-typed example shows how to constrain this even further to a pre-defined list of acceptable currencies.
Example: Validating API Endpoint Paths
Another use case is validating API endpoint paths. You can define a type that represents a valid API endpoint structure, ensuring that requests are made to correct paths. This is especially useful in microservices architectures where multiple services might expose different APIs.
type APIServiceName = "users" | "products" | "orders";
type APIEndpointPath = `/${APIServiceName}/${string}`;
function callAPI(path: APIEndpointPath) {
// API call logic
console.log(`Calling API: ${path}`);
}
callAPI("/users/123"); // Valid
callAPI("/products/details"); // Valid
//callAPI("/invalid/path"); // TypeScript error
// Even more specific:
type APIAction = "create" | "read" | "update" | "delete";
type APIEndpointPathSpecific = `/${APIServiceName}/${APIAction}`;
function callAPISpecific(path: APIEndpointPathSpecific) {
// API call logic
console.log(`Calling specific API: ${path}`);
}
callAPISpecific("/users/create"); // Valid
//callAPISpecific("/users/list"); // TypeScript error
This allows you to define the structure of API endpoints more precisely, preventing typos and ensuring consistency across your application. This is a basic example; more complex patterns can be created to validate query parameters and other parts of the URL.
Benefits of Using Template Literal Types
Using template literal types for string pattern matching and validation offers several benefits:
- Improved Type Safety: Template literal types allow you to enforce stricter type constraints on strings, reducing the risk of runtime errors.
- Enhanced Code Readability: Template literal types make your code more readable by clearly expressing the expected format of strings.
- Increased Maintainability: Template literal types make your code more maintainable by providing a single source of truth for string validation rules.
- Better Developer Experience: Template literal types provide better autocompletion and error messages, improving the overall developer experience.
Limitations
While template literal types are powerful, they also have some limitations:
- Complexity: Template literal types can become complex, especially when dealing with intricate patterns. It's crucial to balance the benefits of type safety with code maintainability.
- Performance: Template literal types can impact compilation performance, especially in large projects. This is because TypeScript needs to perform more complex type checking.
- Limited Regular Expression Support: While template literal types allow for pattern matching, they don't support the full range of regular expression features. For highly complex string validation, runtime regular expressions might still be needed alongside these type constructs for proper input sanitization.
Best Practices
Here are some best practices to keep in mind when using template literal types:
- Start Simple: Begin with simple patterns and gradually increase complexity as needed.
- Use Descriptive Names: Use descriptive names for your template literal types to improve code readability.
- Document Your Types: Document your template literal types to explain their purpose and usage.
- Test Thoroughly: Test your template literal types thoroughly to ensure that they behave as expected.
- Consider Performance: Be mindful of the impact of template literal types on compilation performance and optimize your code accordingly.
Conclusion
Template literal types are a powerful feature in TypeScript that allows you to perform advanced string manipulation, pattern matching, and validation at the type level. By using template literal types, you can create more robust, maintainable, and type-safe applications. While they have some limitations, the benefits of using template literal types often outweigh the drawbacks, making them a valuable tool in any TypeScript developer's arsenal. As the TypeScript language continues to evolve, understanding and utilizing these advanced type features will be crucial for building high-quality software. Remember to balance complexity with readability and always prioritize thorough testing.