Prerequisites
- Understanding of conditional types ๐ฏ
- Knowledge of template literal strings ๐
- Familiarity with type inference โก
What you'll learn
- Master template literal type syntax ๐๏ธ
- Perform string manipulation at type level ๐ง
- Build powerful string-based type utilities ๐จ
- Create sophisticated pattern matching types ๐
๐ฏ Introduction
Welcome to the magical world of template literal types! ๐ Think of template literal types as TypeScriptโs string manipulation workshop ๐ง - they let you build, parse, and transform strings at the type level with the same flexibility you have with regular JavaScript template literals, but with full compile-time type safety!
Youโre about to discover one of TypeScript 4.1โs most creative features - the ability to perform complex string operations, pattern matching, and even build domain-specific languages entirely at the type level. Whether youโre creating type-safe routing systems ๐ฃ๏ธ, building CSS-in-JS libraries ๐จ, or designing elegant APIs ๐, template literal types will give you unprecedented power over string-based type transformations.
By the end of this tutorial, youโll be manipulating strings at the type level like a wizard casting spells! โจ Letโs dive into this string sorcery! ๐ช
๐ Understanding Template Literal Types
๐ค What are Template Literal Types?
Template literal types use the same syntax as JavaScript template literals, but they work at the type level to create new string literal types by combining and manipulating existing types. Think of them as type-safe string builders ๐๏ธ that can generate precise string types at compile-time.
The basic syntax looks like this:
type TemplateType = `prefix${SomeType}suffix`;
This creates a new type that represents all possible string combinations of the template!
๐จ Your First Template Literal Types
Letโs start with simple examples:
// ๐ฏ Basic template literal type
type Greeting = `Hello ${string}!`;
// ๐งช Testing the template
type WelcomeMessage = Greeting; // "Hello ${string}!"
// But when we use it with specific strings...
type PersonalGreeting<T extends string> = `Hello ${T}!`;
type AliceGreeting = PersonalGreeting<"Alice">; // "Hello Alice!"
type BobGreeting = PersonalGreeting<"Bob">; // "Hello Bob!"
// ๐ Multiple template slots
type FullGreeting<First extends string, Last extends string> =
`Hello ${First} ${Last}! Welcome to TypeScript! ๐`;
// โจ Testing multi-slot template
type WelcomeAlice = FullGreeting<"Alice", "Wonder">;
// Result: "Hello Alice Wonder! Welcome to TypeScript! ๐"
// ๐ฎ Template with union types
type Status = "loading" | "success" | "error";
type Prefix = "api" | "user" | "system";
type EventName = `${Prefix}:${Status}`;
// Result: "api:loading" | "api:success" | "api:error" |
// "user:loading" | "user:success" | "user:error" |
// "system:loading" | "system:success" | "system:error"
๐ก The Magic: Template literal types automatically generate all possible combinations when used with union types!
๐ Understanding the Combinatorial Power
// ๐จ Multiple unions create cartesian products
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Resource = "users" | "posts" | "comments";
type Action = "create" | "read" | "update" | "delete";
type Permission = `${HttpMethod}:${Resource}:${Action}`;
// This creates 4 ร 3 ร 4 = 48 different string literal types!
// ๐งช Some examples of the generated types:
// "GET:users:read" | "POST:posts:create" | "DELETE:comments:delete" | ...
// ๐ Using the permission type
function checkPermission(permission: Permission): boolean {
// TypeScript knows exactly which strings are valid!
return permission === "GET:users:read" ||
permission === "POST:posts:create"; // etc.
}
// โ
Type-safe usage
checkPermission("GET:users:read"); // โ
Valid
// checkPermission("INVALID:request"); // โ TypeScript error!
๐ง String Manipulation Patterns
๐ Essential String Transformation Recipes
Letโs explore powerful string manipulation patterns:
// ๐จ Capitalize first letter
type Capitalize<S extends string> =
S extends `${infer First}${infer Rest}`
? `${Uppercase<First>}${Rest}`
: S;
// ๐งช Testing capitalization
type CapitalizedName = Capitalize<"alice">; // "Alice"
type CapitalizedSentence = Capitalize<"hello world">; // "Hello world"
// ๐ Convert snake_case to camelCase
type SnakeToCamel<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
: S;
// ๐ฎ Testing snake case conversion
type CamelCased = SnakeToCamel<"user_profile_settings">; // "userProfileSettings"
type ApiKey = SnakeToCamel<"api_access_token">; // "apiAccessToken"
// โจ Convert camelCase to kebab-case
type CamelToKebab<S extends string> =
S extends `${infer Head}${infer Tail}`
? Tail extends Uncapitalize<Tail>
? `${Uncapitalize<Head>}${CamelToKebab<Tail>}`
: `${Uncapitalize<Head>}-${CamelToKebab<Tail>}`
: S;
// ๐งช Testing kebab case conversion
type KebabCase = CamelToKebab<"UserProfileSettings">; // "user-profile-settings"
type CssClass = CamelToKebab<"primaryButtonLarge">; // "primary-button-large"
// ๐ฏ Extract parts of strings
type GetFileExtension<S extends string> =
S extends `${string}.${infer Extension}`
? Extension
: never;
// ๐ Testing file extension extraction
type JsExt = GetFileExtension<"app.js">; // "js"
type TsExt = GetFileExtension<"main.ts">; // "ts"
type NoExt = GetFileExtension<"README">; // never
// ๐ Get filename without extension
type GetBaseName<S extends string> =
S extends `${infer Name}.${string}`
? Name
: S;
type FileName = GetBaseName<"component.tsx">; // "component"
type PathName = GetBaseName<"utils/helper.js">; // "utils/helper"
๐ฏ Advanced String Pattern Matching
// ๐จ Parse URL patterns
type ParseRoute<S extends string> =
S extends `/${infer Segment}/${infer Rest}`
? Segment | ParseRoute<`/${Rest}`>
: S extends `/${infer LastSegment}`
? LastSegment
: never;
// ๐งช Testing route parsing
type UserRoutes = ParseRoute<"/users/profile/settings">;
// Result: "users" | "profile" | "settings"
// ๐ Extract route parameters
type ExtractParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: S extends `${string}:${infer LastParam}`
? LastParam
: never;
// ๐ฎ Testing parameter extraction
type ApiParams = ExtractParams<"/api/users/:userId/posts/:postId">;
// Result: "userId" | "postId"
// โจ Build type-safe route params object
type RouteParams<S extends string> = {
[K in ExtractParams<S>]: string;
};
// ๐งช Testing route params type
type UserPostParams = RouteParams<"/users/:userId/posts/:postId">;
// Result: { userId: string; postId: string }
// ๐ฏ CSS selector type safety
type CSSSelector<S extends string> =
S extends `.${infer ClassName}`
? { type: "class"; name: ClassName }
: S extends `#${infer IdName}`
? { type: "id"; name: IdName }
: S extends `${infer TagName}`
? { type: "tag"; name: TagName }
: never;
// ๐ Testing CSS selector parsing
type ClassSelector = CSSSelector<".btn-primary">; // { type: "class"; name: "btn-primary" }
type IdSelector = CSSSelector<"#main-content">; // { type: "id"; name: "main-content" }
type TagSelector = CSSSelector<"div">; // { type: "tag"; name: "div" }
๐ก Practical Examples
๐ Example 1: Type-Safe Event System
Letโs build a sophisticated event system using template literal types:
// ๐ฏ Define event namespaces and types
type EventNamespace = "user" | "product" | "order" | "system";
type UserEvent = "login" | "logout" | "register" | "profile-update";
type ProductEvent = "created" | "updated" | "deleted" | "viewed";
type OrderEvent = "placed" | "confirmed" | "shipped" | "delivered" | "cancelled";
type SystemEvent = "startup" | "shutdown" | "error" | "maintenance";
// ๐ Generate all possible event names
type EventName =
| `user:${UserEvent}`
| `product:${ProductEvent}`
| `order:${OrderEvent}`
| `system:${SystemEvent}`;
// ๐จ Event payload types based on event names
type EventPayload<T extends EventName> =
T extends `user:${infer Action}`
? Action extends "login" | "logout"
? { userId: string; timestamp: Date; sessionId: string }
: Action extends "register"
? { userId: string; email: string; username: string }
: Action extends "profile-update"
? { userId: string; changes: Record<string, any> }
: never
: T extends `product:${infer Action}`
? Action extends "created" | "updated"
? { productId: string; name: string; price: number; category: string }
: Action extends "deleted"
? { productId: string; deletedAt: Date }
: Action extends "viewed"
? { productId: string; userId?: string; timestamp: Date }
: never
: T extends `order:${infer Action}`
? Action extends "placed"
? { orderId: string; userId: string; items: Array<{ productId: string; quantity: number }> }
: Action extends "confirmed" | "shipped" | "delivered"
? { orderId: string; timestamp: Date; trackingNumber?: string }
: Action extends "cancelled"
? { orderId: string; reason: string; refundAmount: number }
: never
: T extends `system:${infer Action}`
? Action extends "startup" | "shutdown"
? { timestamp: Date; version: string }
: Action extends "error"
? { error: Error; context: string; severity: "low" | "medium" | "high" }
: Action extends "maintenance"
? { startTime: Date; endTime: Date; reason: string }
: never
: never;
// โจ Type-safe event emitter
class TypeSafeEventEmitter {
private listeners: Map<EventName, Function[]> = new Map();
// ๐ฏ Type-safe event registration
on<T extends EventName>(
eventName: T,
listener: (payload: EventPayload<T>) => void
): void {
const existingListeners = this.listeners.get(eventName) || [];
this.listeners.set(eventName, [...existingListeners, listener]);
console.log(`๐ก Registered listener for ${eventName}`);
}
// ๐ Type-safe event emission
emit<T extends EventName>(
eventName: T,
payload: EventPayload<T>
): void {
const listeners = this.listeners.get(eventName) || [];
listeners.forEach(listener => {
(listener as (payload: EventPayload<T>) => void)(payload);
});
console.log(`๐ Emitted ${eventName} with payload:`, payload);
}
// ๐ฎ Remove listener
off<T extends EventName>(
eventName: T,
listener: (payload: EventPayload<T>) => void
): void {
const listeners = this.listeners.get(eventName) || [];
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
this.listeners.set(eventName, listeners);
console.log(`๐ Removed listener for ${eventName}`);
}
}
// โจ Get all listeners for debugging
getListeners(): Map<EventName, number> {
const listenerCounts = new Map<EventName, number>();
for (const [eventName, listeners] of this.listeners) {
listenerCounts.set(eventName, listeners.length);
}
return listenerCounts;
}
}
// ๐งช Usage example
const eventEmitter = new TypeSafeEventEmitter();
// โ
Type-safe event registration
eventEmitter.on("user:login", (payload) => {
// TypeScript knows payload is { userId: string; timestamp: Date; sessionId: string }
console.log(`๐ User ${payload.userId} logged in with session ${payload.sessionId}`);
});
eventEmitter.on("product:created", (payload) => {
// TypeScript knows payload is { productId: string; name: string; price: number; category: string }
console.log(`๐๏ธ New product created: ${payload.name} ($${payload.price})`);
});
eventEmitter.on("order:placed", (payload) => {
// TypeScript knows the exact shape of order payload
console.log(`๐ฆ Order ${payload.orderId} placed by user ${payload.userId}`);
console.log(`Items: ${payload.items.length} products`);
});
// ๐ Type-safe event emission
eventEmitter.emit("user:login", {
userId: "user123",
timestamp: new Date(),
sessionId: "session456"
});
eventEmitter.emit("product:created", {
productId: "prod789",
name: "TypeScript Guide",
price: 29.99,
category: "books"
});
// โ These would cause TypeScript errors:
// eventEmitter.emit("user:login", { wrongPayload: true }); // Wrong payload type
// eventEmitter.emit("invalid:event", { data: "test" }); // Invalid event name
๐ฎ Example 2: CSS-in-JS Type Safety
Letโs create a type-safe CSS-in-JS system:
// ๐ CSS property types
type CSSLength = `${number}px` | `${number}em` | `${number}rem` | `${number}%` | `${number}vh` | `${number}vw`;
type CSSColor = `#${string}` | `rgb(${number}, ${number}, ${number})` | `rgba(${number}, ${number}, ${number}, ${number})`;
// ๐ฏ CSS property names
type CSSProperty =
| "margin" | "padding" | "border"
| "width" | "height" | "max-width" | "max-height" | "min-width" | "min-height"
| "color" | "background-color" | "border-color"
| "font-size" | "font-weight" | "line-height"
| "display" | "position" | "top" | "right" | "bottom" | "left"
| "flex-direction" | "justify-content" | "align-items";
// ๐ Generate responsive CSS properties
type Breakpoint = "sm" | "md" | "lg" | "xl";
type ResponsiveProp<T extends CSSProperty> = T | `${T}-${Breakpoint}`;
// ๐จ CSS class naming patterns
type BEMElement<B extends string, E extends string> = `${B}__${E}`;
type BEMModifier<B extends string, M extends string> = `${B}--${M}`;
type BEMElementModifier<B extends string, E extends string, M extends string> = `${B}__${E}--${M}`;
// โจ Complete BEM class name type
type BEMClass<
Block extends string,
Element extends string = never,
Modifier extends string = never
> = [Element] extends [never]
? [Modifier] extends [never]
? Block
: BEMModifier<Block, Modifier>
: [Modifier] extends [never]
? BEMElement<Block, Element>
: BEMElementModifier<Block, Element, Modifier>;
// ๐งช Testing BEM class generation
type ButtonClass = BEMClass<"button">; // "button"
type ButtonPrimary = BEMClass<"button", never, "primary">; // "button--primary"
type ButtonIcon = BEMClass<"button", "icon">; // "button__icon"
type ButtonIconSmall = BEMClass<"button", "icon", "small">; // "button__icon--small"
// ๐ฎ Type-safe CSS-in-JS implementation
interface CSSRule {
[key: string]: string | number | CSSRule;
}
type StyleObject = {
[P in CSSProperty]?: string | number;
} & {
[key: string]: string | number | StyleObject;
};
// ๐ CSS class generator with type safety
class TypeSafeCSSGenerator {
private styles: Map<string, StyleObject> = new Map();
private generatedCSS: string[] = [];
// ๐ฏ Add BEM-style class
addBEMClass<
B extends string,
E extends string = never,
M extends string = never
>(
block: B,
element?: E,
modifier?: M,
styles: StyleObject = {}
): BEMClass<B, E, M> {
const className = this.generateBEMClassName(block, element, modifier);
this.styles.set(className, styles);
return className as BEMClass<B, E, M>;
}
// โจ Generate responsive classes
addResponsiveClass<T extends string>(
baseName: T,
baseStyles: StyleObject,
responsiveStyles: Partial<Record<Breakpoint, StyleObject>>
): T {
// Add base class
this.styles.set(baseName, baseStyles);
// Add responsive variants
Object.entries(responsiveStyles).forEach(([breakpoint, styles]) => {
const responsiveClassName = `${baseName}-${breakpoint}`;
this.styles.set(responsiveClassName, styles as StyleObject);
});
return baseName;
}
// ๐จ Generate CSS string
generateCSS(): string {
const cssRules: string[] = [];
for (const [className, styles] of this.styles) {
const cssRule = this.generateCSSRule(className, styles);
cssRules.push(cssRule);
}
return cssRules.join('\n\n');
}
// ๐ง Private helper methods
private generateBEMClassName<
B extends string,
E extends string = never,
M extends string = never
>(block: B, element?: E, modifier?: M): string {
let className = block;
if (element && element !== 'never' as any) {
className += `__${element}`;
}
if (modifier && modifier !== 'never' as any) {
className += `--${modifier}`;
}
return className;
}
private generateCSSRule(className: string, styles: StyleObject): string {
const properties: string[] = [];
for (const [property, value] of Object.entries(styles)) {
if (typeof value === 'object') {
// Nested rules (pseudo-classes, media queries, etc.)
continue; // Simplified for this example
} else {
properties.push(` ${this.kebabCase(property)}: ${value};`);
}
}
return `.${className} {\n${properties.join('\n')}\n}`;
}
private kebabCase(str: string): string {
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}
}
// ๐งช Usage example
const cssGenerator = new TypeSafeCSSGenerator();
// โ
Type-safe BEM class creation
const buttonClass = cssGenerator.addBEMClass("button", undefined, undefined, {
padding: "10px 20px",
borderRadius: "4px",
border: "none",
cursor: "pointer"
});
const buttonPrimaryClass = cssGenerator.addBEMClass("button", undefined, "primary", {
backgroundColor: "#007bff",
color: "white"
});
const buttonIconClass = cssGenerator.addBEMClass("button", "icon", undefined, {
width: "20px",
height: "20px",
marginRight: "8px"
});
// ๐ Type-safe responsive classes
const containerClass = cssGenerator.addResponsiveClass("container", {
width: "100%",
padding: "0 16px"
}, {
sm: { maxWidth: "540px" },
md: { maxWidth: "720px" },
lg: { maxWidth: "960px" },
xl: { maxWidth: "1140px" }
});
// ๐ฏ Generate final CSS
const generatedCSS = cssGenerator.generateCSS();
console.log("๐จ Generated CSS:");
console.log(generatedCSS);
// TypeScript knows the exact class names!
console.log("โ
Class names:", {
button: buttonClass, // "button"
buttonPrimary: buttonPrimaryClass, // "button--primary"
buttonIcon: buttonIconClass, // "button__icon"
container: containerClass // "container"
});
๐ Advanced Template Literal Techniques
๐งโโ๏ธ Recursive String Processing
// ๐จ Parse path segments recursively
type ParsePath<T extends string> =
T extends `${infer Segment}/${infer Rest}`
? [Segment, ...ParsePath<Rest>]
: T extends ""
? []
: [T];
// ๐งช Testing path parsing
type ApiPath = ParsePath<"api/v1/users/123/posts">;
// Result: ["api", "v1", "users", "123", "posts"]
// ๐ Join path segments
type JoinPath<T extends readonly string[]> =
T extends readonly [infer First, ...infer Rest]
? First extends string
? Rest extends readonly string[]
? Rest["length"] extends 0
? First
: `${First}/${JoinPath<Rest>}`
: never
: never
: "";
// ๐ฎ Testing path joining
type ReconstructedPath = JoinPath<["api", "v1", "users"]>; // "api/v1/users"
// โจ Deep object path type generation
type DeepKeyOf<T> = T extends Record<string, any>
? {
[K in keyof T]: K extends string
? T[K] extends Record<string, any>
? `${K}` | `${K}.${DeepKeyOf<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
// ๐งช Testing deep key extraction
interface NestedConfig {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
baseUrl: string;
timeout: number;
};
}
type ConfigPaths = DeepKeyOf<NestedConfig>;
// Result: "database" | "database.host" | "database.port" |
// "database.credentials" | "database.credentials.username" |
// "database.credentials.password" | "api" | "api.baseUrl" | "api.timeout"
๐๏ธ String Validation Patterns
// ๐ฏ Email validation pattern
type IsEmail<T extends string> =
T extends `${string}@${string}.${string}`
? true
: false;
// ๐งช Testing email validation
type ValidEmail = IsEmail<"[email protected]">; // true
type InvalidEmail = IsEmail<"not-an-email">; // false
// ๐ URL validation pattern
type IsURL<T extends string> =
T extends `http${"s" | ""}://${string}.${string}`
? true
: false;
// ๐ฎ Testing URL validation
type ValidURL = IsURL<"https://example.com">; // true
type InvalidURL = IsURL<"not-a-url">; // false
// โจ Version string validation
type IsVersion<T extends string> =
T extends `${number}.${number}.${number}`
? true
: false;
// ๐งช Testing version validation
type ValidVersion = IsVersion<"1.2.3">; // true
type InvalidVersion = IsVersion<"1.2">; // false
// ๐ Complex pattern matching
type ParseSemVer<T extends string> =
T extends `${infer Major}.${infer Minor}.${infer Patch}`
? {
major: Major;
minor: Minor;
patch: Patch;
full: T;
}
: never;
// ๐ Testing semantic version parsing
type VersionInfo = ParseSemVer<"2.1.4">;
// Result: { major: "2"; minor: "1"; patch: "4"; full: "2.1.4" }
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Template Literal Explosion
// โ Too many combinations can cause performance issues
type TooBig = `${string}-${string}-${string}-${string}`;
// This can create infinite possibilities and slow down TypeScript
// โ
Use specific union types instead
type Size = "small" | "medium" | "large";
type Color = "red" | "blue" | "green";
type ButtonClass = `btn-${Size}-${Color}`; // Only 9 combinations
๐คฏ Pitfall 2: Infinite Recursion
// โ Recursive types without proper termination
type BadRecursive<T extends string> =
T extends `${infer Head}${infer Tail}`
? Head | BadRecursive<Tail> // This might never terminate!
: never;
// โ
Add depth limits or proper termination conditions
type SafeRecursive<T extends string, Depth extends any[] = []> =
Depth["length"] extends 10 // Limit recursion depth
? never
: T extends `${infer Head}${infer Tail}`
? Head | SafeRecursive<Tail, [...Depth, any]>
: never;
๐ Pitfall 3: Case Sensitivity Issues
// โ Not handling different cases
type BadRouteMatch<T> = T extends "/users" ? true : false;
// โ
Handle case variations
type CaseInsensitiveMatch<T extends string, U extends string> =
Lowercase<T> extends Lowercase<U> ? true : false;
type FlexibleRouteMatch = CaseInsensitiveMatch<"/Users", "/users">; // true
๐ ๏ธ Best Practices
- ๐ฏ Keep Templates Simple: Avoid overly complex template patterns
- ๐ Use Union Types: Prefer specific unions over
string
for better type safety - ๐ก๏ธ Limit Recursion: Set depth limits to prevent infinite recursion
- ๐จ Test Edge Cases: Always test with empty strings and edge cases
- โจ Document Complex Patterns: Complex template types can be hard to understand
- ๐ Consider Performance: Very complex templates can slow down type checking
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe SQL Query Builder
Create a sophisticated SQL query builder using template literal types:
๐ Requirements:
- โ Type-safe table and column names
- ๐ง WHERE clause construction with proper operators
- ๐จ JOIN support with relationship validation
- ๐ ORDER BY and GROUP BY clauses
- โจ Type-safe result mapping!
๐ Bonus Points:
- Add subquery support
- Create aggregate function types
- Build migration type checking
๐ก Solution
๐ Click to see solution
// ๐ฏ Database schema definition
interface DatabaseSchema {
users: {
id: number;
username: string;
email: string;
age: number;
created_at: Date;
is_active: boolean;
};
posts: {
id: number;
title: string;
content: string;
author_id: number;
published: boolean;
created_at: Date;
view_count: number;
};
comments: {
id: number;
post_id: number;
author_id: number;
content: string;
created_at: Date;
};
}
// ๐ Generate qualified column names
type QualifiedColumn<
Table extends keyof DatabaseSchema,
Column extends keyof DatabaseSchema[Table]
> = `${string & Table}.${string & Column}`;
// ๐จ SQL operators for WHERE clauses
type SQLOperator = "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
// โจ WHERE clause condition
type WhereCondition<
Table extends keyof DatabaseSchema,
Column extends keyof DatabaseSchema[Table]
> = `${QualifiedColumn<Table, Column>} ${SQLOperator} ?`;
// ๐งช JOIN types
type JoinType = "INNER JOIN" | "LEFT JOIN" | "RIGHT JOIN" | "FULL OUTER JOIN";
// ๐ฎ Generate JOIN clause
type JoinClause<
FromTable extends keyof DatabaseSchema,
ToTable extends keyof DatabaseSchema,
FromColumn extends keyof DatabaseSchema[FromTable],
ToColumn extends keyof DatabaseSchema[ToTable]
> = `${JoinType} ${string & ToTable} ON ${QualifiedColumn<FromTable, FromColumn>} = ${QualifiedColumn<ToTable, ToColumn>}`;
// ๐ฏ ORDER BY direction
type OrderDirection = "ASC" | "DESC";
// ๐ ORDER BY clause
type OrderByClause<
Table extends keyof DatabaseSchema,
Column extends keyof DatabaseSchema[Table]
> = `${QualifiedColumn<Table, Column>} ${OrderDirection}`;
// ๐จ SELECT clause builder
type SelectClause<Tables extends keyof DatabaseSchema> = {
[T in Tables]: {
[C in keyof DatabaseSchema[T]]: QualifiedColumn<T, C>;
};
}[Tables][keyof DatabaseSchema[Tables]];
// โจ Type-safe SQL query builder
class TypeSafeSQLBuilder<PrimaryTable extends keyof DatabaseSchema> {
private selectColumns: string[] = [];
private fromTable: string;
private joins: string[] = [];
private whereConditions: string[] = [];
private orderByColumns: string[] = [];
private groupByColumns: string[] = [];
private havingConditions: string[] = [];
constructor(table: PrimaryTable) {
this.fromTable = table as string;
}
// ๐ฏ Type-safe SELECT
select<T extends PrimaryTable, C extends keyof DatabaseSchema[T]>(
...columns: Array<C | QualifiedColumn<T, C> | "*">
): this {
columns.forEach(col => {
if (col === "*") {
this.selectColumns.push("*");
} else {
this.selectColumns.push(String(col));
}
});
return this;
}
// ๐ Type-safe JOIN
join<
ToTable extends keyof DatabaseSchema,
FromColumn extends keyof DatabaseSchema[PrimaryTable],
ToColumn extends keyof DatabaseSchema[ToTable]
>(
joinType: JoinType,
toTable: ToTable,
fromColumn: FromColumn,
toColumn: ToColumn
): TypeSafeSQLBuilder<PrimaryTable> {
const joinClause = `${joinType} ${String(toTable)} ON ${this.fromTable}.${String(fromColumn)} = ${String(toTable)}.${String(toColumn)}`;
this.joins.push(joinClause);
return this;
}
// ๐จ Type-safe WHERE
where<
T extends PrimaryTable,
C extends keyof DatabaseSchema[T]
>(
column: C | QualifiedColumn<T, C>,
operator: SQLOperator,
value: DatabaseSchema[T][C] | DatabaseSchema[T][C][]
): this {
const condition = `${String(column)} ${operator} ${this.formatValue(value)}`;
this.whereConditions.push(condition);
return this;
}
// โจ Type-safe ORDER BY
orderBy<
T extends PrimaryTable,
C extends keyof DatabaseSchema[T]
>(
column: C | QualifiedColumn<T, C>,
direction: OrderDirection = "ASC"
): this {
this.orderByColumns.push(`${String(column)} ${direction}`);
return this;
}
// ๐งช Type-safe GROUP BY
groupBy<
T extends PrimaryTable,
C extends keyof DatabaseSchema[T]
>(
...columns: Array<C | QualifiedColumn<T, C>>
): this {
columns.forEach(col => {
this.groupByColumns.push(String(col));
});
return this;
}
// ๐ฎ Build final SQL query
build(): string {
let query = "SELECT ";
// SELECT clause
query += this.selectColumns.length > 0 ? this.selectColumns.join(", ") : "*";
// FROM clause
query += ` FROM ${this.fromTable}`;
// JOIN clauses
if (this.joins.length > 0) {
query += " " + this.joins.join(" ");
}
// WHERE clause
if (this.whereConditions.length > 0) {
query += " WHERE " + this.whereConditions.join(" AND ");
}
// GROUP BY clause
if (this.groupByColumns.length > 0) {
query += " GROUP BY " + this.groupByColumns.join(", ");
}
// ORDER BY clause
if (this.orderByColumns.length > 0) {
query += " ORDER BY " + this.orderByColumns.join(", ");
}
return query;
}
// ๐ง Private helper methods
private formatValue(value: any): string {
if (Array.isArray(value)) {
return `(${value.map(v => typeof v === 'string' ? `'${v}'` : String(v)).join(', ')})`;
}
return typeof value === 'string' ? `'${value}'` : String(value);
}
}
// ๐ SQL query factory
class SQLQueryFactory {
static from<T extends keyof DatabaseSchema>(table: T): TypeSafeSQLBuilder<T> {
return new TypeSafeSQLBuilder(table);
}
// ๐ฏ Predefined common queries
static getUserWithPosts(userId: number): string {
return this.from("users")
.select("users.id", "users.username", "users.email", "posts.title", "posts.content")
.join("INNER JOIN", "posts", "id", "author_id")
.where("users.id", "=", userId)
.orderBy("posts.created_at", "DESC")
.build();
}
static getActiveUsersWithCommentCount(): string {
return this.from("users")
.select("users.username", "users.email")
.join("LEFT JOIN", "comments", "id", "author_id")
.where("users.is_active", "=", true)
.groupBy("users.id", "users.username", "users.email")
.orderBy("users.username", "ASC")
.build();
}
}
// ๐งช Usage examples
const queryBuilder = SQLQueryFactory.from("users");
// โ
Type-safe query building
const userQuery = queryBuilder
.select("id", "username", "email") // TypeScript knows these are valid columns
.where("is_active", "=", true) // TypeScript enforces correct value type
.where("age", ">=", 18) // TypeScript knows age is number
.orderBy("created_at", "DESC") // TypeScript knows created_at exists
.build();
console.log("๐ค User Query:");
console.log(userQuery);
// ๐ Complex query with joins
const postQuery = SQLQueryFactory.from("posts")
.select("posts.title", "posts.content", "users.username")
.join("INNER JOIN", "users", "author_id", "id")
.where("posts.published", "=", true)
.where("posts.view_count", ">", 100)
.orderBy("posts.created_at", "DESC")
.build();
console.log("๐ Post Query:");
console.log(postQuery);
// โจ Predefined queries
const userWithPostsQuery = SQLQueryFactory.getUserWithPosts(123);
console.log("๐ User with Posts Query:");
console.log(userWithPostsQuery);
// โ These would cause TypeScript errors:
// queryBuilder.select("invalid_column"); // Column doesn't exist
// queryBuilder.where("age", "=", "string"); // Wrong type for age
// queryBuilder.join("INNER JOIN", "invalid_table", "id", "id"); // Table doesn't exist
console.log("๐ Type-safe SQL query building complete!");
๐ Key Takeaways
Youโve mastered template literal types! Hereโs what you can now do:
- โ Perform complex string manipulation at the type level ๐ช
- โ Build sophisticated pattern matching systems with precision ๐ก๏ธ
- โ Create type-safe APIs based on string patterns ๐ฏ
- โ Design domain-specific languages in TypeScript ๐
- โ Generate combinatorial type systems that scale beautifully ๐
Remember: Template literal types are like having a string manipulation laboratory ๐งช at the type level - you can build, parse, and transform strings with complete type safety!
๐ค Next Steps
Congratulations! ๐ Youโve conquered template literal types!
Hereโs what to explore next:
- ๐ป Practice with the SQL query builder exercise above
- ๐๏ธ Build your own string-based type utilities for real projects
- ๐ Explore combining template literals with other advanced type features
- ๐ Share your string manipulation patterns with the TypeScript community!
You now possess one of TypeScriptโs most creative and powerful tools. Use it to create type systems that are both expressive and safe. Remember - every type-level string wizard started with simple templates. Keep experimenting, keep building, and most importantly, have fun crafting magical string types! ๐โจ
Happy string type crafting! ๐๐ญโจ