Parsing at the Type Level
TypeScript’s template literal types, combined with recursive conditional types and infer, allow you to parse and transform string literals entirely within the type system. This is not a runtime parser—it is a compile-time parser that extracts structure from string constants, enabling strongly typed APIs for URL routing, SQL queries, command-line arguments, and more.
Template Literal Types Primer
Template literal types can interpolate other types and infer parts of a string.
type Hello<T extends string> = `hello ${T}`;
type Result = Hello<"world">; // "hello world"
With infer inside a conditional type, you can extract substrings:
type ExtractName<T extends string> =
T extends `hello ${infer Name}` ? Name : never;
type A = ExtractName<"hello alice">; // "alice"
type B = ExtractName<"goodbye bob">; // never
Parsing URL Route Parameters
A common use case is extracting path parameters from a route pattern:
type RouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof RouteParams<Rest>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {};
This recursively splits the string at each : prefix and builds an object type.
type Params = RouteParams<"/users/:id/posts/:postId">;
// { id: string; postId: string }
A More Robust Route Parser
The naive version above breaks with optional segments or regex constraints. A production-grade parser handles these edge cases:
type ParseParam<T extends string> =
T extends `${infer _Prefix}:${infer Param}`
? Param extends `${infer Name}?`
? { [K in Name]?: string }
: { [K in Name]: string }
: {};
type Merge<T, U> = { [K in keyof T | keyof U]: K extends keyof T ? T[K] : K extends keyof U ? U[K] : never };
type ParseRoute<T extends string> =
T extends `${infer Segment}/${infer Rest}`
? Merge<ParseParam<Segment>, ParseRoute<Rest>>
: ParseParam<T>;
Optional parameters (?) become optional properties in the resulting type.
Extracting Query Strings
Query string parsing follows the same pattern:
type ParseQuery<T extends string> =
T extends `${infer First}&${infer Rest}`
? Merge<ParseSingle<First>, ParseQuery<Rest>>
: ParseSingle<T>;
type ParseSingle<T extends string> =
T extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};
type Q = ParseQuery<"page=1&limit=10&sort=asc">;
// { page: "1"; limit: "10"; sort: "asc" }
For real-world use, combine with mapping types to convert values:
type ParseInt<T extends string> =
T extends `${infer N extends number}` ? N : never;
type TypedQuery = {
[K in keyof Q]: K extends "page" | "limit" ? ParseInt<Q[K]> : Q[K];
};
// { page: 1; limit: 10; sort: "asc" }
Practical API Typing Pattern
The real power comes when you bind the parser to an API function:
function get<T extends string>(
path: T,
params: ParseRoute<T>
): void;
get("/users/:id/posts/:postId", {
id: "42",
postId: "99"
}); // OK
get("/users/:id", {
id: "42",
extra: "x"
});
// Error: 'extra' does not exist in { id: string }
Libraries like typed-query-parser, ts-toolbelt, and Hono use these techniques to provide end-to-end type safety without runtime overhead.
Limitations
| Issue | Impact |
|---|---|
| Deep recursion limit (~50 levels) | Breaks on very long strings |
| No regex support | Cannot validate patterns like /^[a-z]+$/ |
| No union splitting | Hard to parse `a |
| Compile-time cost | Complex parsers slow down tsc |
Summary
Type-level parsers transform string literals into structured types at compile time. They enable fully typed URL routing, query string extraction, and configuration parsing without code generation. Use recursive conditional types with infer and template literals to peel apart strings character by character. Keep an eye on recursion depth and compile-time performance—but for typical routing use cases, the pattern is production-ready.
