Template literal types in TypeScript provide a powerful way to create new string types by embedding expressions within string literals. This feature allows for a more dynamic and expressive type system, making it possible to define complex types and enhance type safety in TypeScript applications. In this chapter, we'll explore the concept of template literal types in detail, from basic usage to advanced applications, with plenty of examples to illustrate their use.
Template literal types are a TypeScript feature that allows you to create types based on string literal templates. These types are constructed by embedding expressions inside backticks (`
) using the ${}
syntax, similar to JavaScript’s template literals.
The basic syntax of template literal types involves combining string literal types with the ${}
syntax to produce new string types. Here’s an example:
type Greeting = `Hello, ${string}!`;
// Example usage
const greet: Greeting = "Hello, TypeScript!";
In this example, Greeting
is a type that represents any string that starts with “Hello, ” and ends with “!”. The ${string}
placeholder indicates that any string can be placed in that position.
type Greeting = `Hello, ${string}!`;
const greet1: Greeting = "Hello, TypeScript!"; // Valid
const greet2: Greeting = "Hello, world!"; // Valid
const greet3: Greeting = "Hi, TypeScript!"; // Error: Type '"Hi, TypeScript!"' is not assignable to type 'Greeting'.
Here, greet1
and greet2
are valid instances of the Greeting
type because they match the template. greet3
is not valid because it doesn’t match the required format.
You can combine template literal types with union types to create more complex types. This allows for greater flexibility and specificity.
type Color = 'red' | 'green' | 'blue';
type ColoredItem = `${Color} item`;
// Example usage
const item1: ColoredItem = "red item"; // Valid
const item2: ColoredItem = "green item"; // Valid
const item3: ColoredItem = "yellow item"; // Error: Type '"yellow item"' is not assignable to type 'ColoredItem'.
In this example, ColoredItem
can be any string that combines a color (from the Color
type) with the word “item”.
You can also extend template literal types to create more specific types based on existing ones.
type BaseURL = "http://example.com";
type Endpoint = "/users" | "/posts" | "/comments";
type APIURL = `${BaseURL}${Endpoint}`;
// Example usage
const url1: APIURL = "http://example.com/users"; // Valid
const url2: APIURL = "http://example.com/posts"; // Valid
const url3: APIURL = "http://example.com/products"; // Error: Type '"http://example.com/products"' is not assignable to type 'APIURL'.
In this example, APIURL
represents URLs that combine the base URL with a specific endpoint.
Template literal types can be recursive, allowing for the creation of highly dynamic types.
type Path = '/' | `/${string}/${Path}`;
// Example usage
const path1: Path = "/"; // Valid
const path2: Path = "/users"; // Valid
const path3: Path = "/users/123"; // Valid
const path4: Path = "/users/123/posts"; // Valid
const path5: Path = "/users/123/posts/"; // Error: Type '"/users/123/posts/"' is not assignable to type 'Path'.
In this example, Path
can represent nested paths with arbitrary depth.
You can use conditional types in conjunction with template literal types to create even more flexible and powerful type definitions.
type ExtractName = T extends `Hello, ${infer U}!` ? U : never;
// Example usage
type Name1 = ExtractName<'Hello, John!'>; // Type is 'John'
type Name2 = ExtractName<'Hello, world!'>; // Type is 'world'
type Name3 = ExtractName<'Hi, there!'>; // Type is 'never'
Here, ExtractName
extracts the name from a greeting string, if it matches the pattern.
Template literal types can be used to define dynamic object keys, enhancing type safety for objects with variable key names.
type Event = 'click' | 'hover' | 'focus';
type EventHandler = `${Event}Handler`;
interface Handlers {
clickHandler: () => void;
hoverHandler: () => void;
focusHandler: () => void;
}
// Example usage
const handlers: Handlers = {
clickHandler: () => console.log("Clicked"),
hoverHandler: () => console.log("Hovered"),
focusHandler: () => console.log("Focused"),
};
In this example, the Handlers
interface ensures that all event handlers follow a consistent naming pattern.
Template literal types can also be used to create more expressive and type-safe APIs.
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type URL = `/${string}`;
type APIEndpoint = `${Method} ${URL}`;
function callAPI(endpoint: APIEndpoint) {
console.log(`Calling API endpoint: ${endpoint}`);
}
// Example usage
callAPI('GET /users'); // Valid
callAPI('POST /users'); // Valid
callAPI('PUT /users/123'); // Valid
callAPI('FETCH /users'); // Error: Type '"FETCH /users"' is not assignable to type 'APIEndpoint'.
Here, APIEndpoint
ensures that API calls are made with valid methods and URLs.
While template literal types are powerful, they can become overly complex and hard to maintain. Always strive for a balance between type safety and readability.
Use TypeScript’s type assertion features and the as const
assertion to test and verify complex template literal types.
type Event = 'click' | 'hover' | 'focus';
type EventHandler = `${Event}Handler`;
// Test function
function testHandler(handler: EventHandler) {
console.log(`Handler: ${handler}`);
}
// Example test
testHandler('clickHandler'); // Valid
testHandler('dragHandler'); // Error: Argument of type '"dragHandler"' is not assignable to parameter of type 'EventHandler'.
Template literal types in TypeScript provide a robust toolset for creating dynamic and expressive types. They allow developers to define complex string patterns, ensuring greater type safety and enhancing the maintainability of the codebase. By leveraging template literal types, you can build more flexible and reliable TypeScript applications. As you continue to explore and experiment with these types, you'll uncover new possibilities for improving your code's expressiveness and safety. Happy coding! ❤️