Unlocking TypeScript’s Template Literal Types: From Simple Strings to Powerful Schema Parsers
This article explores TypeScript’s template literal types, demonstrates how they can parse route parameters and custom string schemas, and builds a generic utility called Keeper for safe object access and transformation, showcasing advanced type‑level programming techniques.
TypeScript 4.1 added template literal types , which allow string manipulation at the type level, mirroring JavaScript template strings.
Template Literal Types
Example:
type World = 'world';
type Greeting = `hello ${World}`; // "hello world"Route Parameter Extraction
A generic type can infer the parameter names from an Express‑style route string. The implementation recursively matches the route pattern and builds a ParamsDictionary type.
type RouteParameters<Route extends string> =
string extends Route ? ParamsDictionary :
Route extends `${${string}}(${${string}` ? ParamsDictionary :
Route extends `${${infer Key}:${infer Rest}}` ?
(GetRouteParameter<Rest> extends never ? ParamsDictionary :
GetRouteParameter<Rest> extends `${infer ParamName}?` ?
{ [P in ParamName]?: string } :
{ [P in GetRouteParameter<Rest>]: string }) &
(Rest extends `${GetRouteParameter<Rest>}${infer Next}` ?
RouteParameters<Next> : unknown)
: {};String Schema Parsing
Using template literal types together with infer, a family of types converts a textual schema into a concrete object type.
Single‑line entries of the form "name string" become { name: string }. The core helpers are: GetType<T extends string> – resolves primitive types, array syntax ( int[]), and reference syntax ( *User). ParseLine<T extends string, Includes = {}> – parses one line ( key type) and applies GetType.
ParseSchema<Str extends string, Includes = {}, Origins = {}>– recursively splits a multi‑line schema on newline characters, merging each ParseLine result.
Example for a simple schema:
type ParseSchema<T extends string> =
T extends `${infer Key} ${infer Type}`
? { [K in Key]: Type extends 'string' ? string :
Type extends 'number' ? number : never }
: {};
type Result = ParseSchema<'name string'>; // { name: string }Array support:
type GetType<T extends string> =
T extends `${infer Elem}[]` ? GetType<Elem>[] :
T extends 'string' ? string :
T extends 'number' ? number : never;
type Result = ParseSchema<'tags string[]'>; // { tags: string[] }Nested structures and type references are handled by extending GetType with a map of included types and the *<Name> syntax. For example, with a previously defined UserInfo type:
type GetType<Str extends string, Includes extends {} = {}> =
Str extends `${infer Elem}[]` ? GetType<Elem, Includes>[] :
Str extends keyof TypeTransformMap ? TypeTransformMap[Str] :
Str extends `*${infer Ref}` ? Ref extends keyof Includes ? Includes[Ref] : never :
never;Keeper Utility (runtime wrapper)
The createKeeper function consumes a schema string and returns an object with two type‑safe methods: from(obj) – builds a new object that conforms to the schema, applying default values for missing required fields and performing type conversions (e.g., string → number). read(obj, path) – safely retrieves a nested value using a dot‑notation path, with full compile‑time inference (similar to lodash.get).
Schema syntax supports optional extensions: renamefrom:sourceProp – maps a source property name to the target name. copyas:alias – creates an alias property with the same type. *<OtherSchema> – references another schema (including array forms like *User[]).
Example usage:
const User = createKeeper(`
name string
age int renamefrom:user_age
`);
const data = User.from({ name: "bruce", user_age: "18" });
// data => { name: "bruce", age: 18 }
const age = User.read(data, "age"); // 18 (type number)Nested schemas can be composed by passing an extends map:
const UserInfo = createKeeper(`
name string
age int renamefrom:user_age
`);
const Human = createKeeper(`
id int
scores float[]
info *UserInfo
`, { extends: { UserInfo } });
const src = {
id: "1",
scores: ["80.5", "90"],
info: { name: "bruce", user_age: "18" }
};
const id = Human.read(src, "id"); // 1 (number)
const firstScore = Human.read(src, "scores[0]"); // 80.5 (number)
const userName = Human.read(src, "info.name"); // "bruce"The from method also supplies defaults for undefined or null fields based on the declared type, ensuring the returned object always matches the compile‑time schema.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
