Template Literal Types in TypeScript
I once spent an afternoon debugging a function that expected a URL string but received a bare domain name. No protocol, no slashes. The code ran fine unti...
23 Apr 2024

I once spent an afternoon debugging a function that expected a URL string but received a bare domain name. No protocol, no slashes. The code ran fine until it didn't.
Template literal types let you encode string structure into the type system. Instead of accepting any string, you describe the shape a string must have. The compiler rejects anything that doesn't match.
What they do
A template literal type works like a template literal in JavaScript, but at the type level. You define a pattern, and TypeScript enforces it at compile time.
type URL = `${string}://${string}`;
const valid: URL = "https://gazar.dev"; // works
const broken: URL = "gazar.dev"; // compile error
The type URL says: "some string, then ://, then some string." Anything else is rejected before the code ever runs.
More patterns
You can mix string and number placeholders to model real formats:
type CreditCardSuffix = `XXXX-XXXX-XXXX-${number}`;
type EmailAddress = `${string}@${string}.${string}`;
type DateString = `${number}-${number}-${number}`;
type IpAddress = `${number}.${number}.${number}.${number}`;
You can also combine them with union types:
type Color = "red" | "green" | "blue";
type CSSColor = `--color-${Color}`;
// results in "--color-red" | "--color-green" | "--color-blue"
The trade-off
Template literal types catch structural mistakes early. That's the benefit.
The cost: they can only validate shape, not semantics. "999.999.999.999" passes the IpAddress type above just fine. And complex patterns can make type errors hard to read.
Use them when the string format matters and the pattern is simple. For deep validation, runtime checks are still your friend.