Fundamentals 12 min read

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.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Unlocking TypeScript’s Template Literal Types: From Simple Strings to Powerful Schema Parsers

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScripttemplate literal typesGeneric TypesUtility LibrarySafe Object AccessSchema Parsing
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.