tsa-composer
v3.0.3
Published
A lightweight, flexible TypeScript utility for composing template string tag functions with custom parsers and transformations.
Readme
TSA Composer
A lightweight, flexible TypeScript utility for composing template string tag functions with custom parsers and transformations.
Overview
TSA Composer allows you to create custom tagged template literal functions that process template strings and their interpolated values. It provides a clean, composable API for building powerful string processing utilities with full type safety.
Key features:
- Type-safe template literal tag composition
- Built-in string joiner for common use cases
- Custom parser support for advanced transformations
- Curried API for elegant function composition
- Zero dependencies
- Tiny footprint
Installation
# Using npm
npm install tsa-composer
# Using yarn
yarn add tsa-composer
# Using bun
bun add tsa-composerThe Problem TSA Composer Solves
Ever wanted to use your existing functions as tagged template literals without rewriting them? TSA Composer makes it trivial.
Without TSA Composer - You'd need to refactor your function:
// Your existing function
function greet(name: string) {
return `Hello, ${name}!`;
}
// Call it normally
greet("Alice"); // ✓ Works
// Want to use it as a tag? You need to rewrite it!
greet`Alice`; // ✗ Doesn't work
// You'd have to create a new function:
function greetTag(tsa: TemplateStringsArray, ...values: string[]) {
const name = tsa.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
return greet(name); // Lots of boilerplate!
}With TSA Composer - Just wrap it:
// Your existing function (unchanged!)
function greet(name: string) {
return `Hello, ${name}!`;
}
// Make it accept template literals - one line!
const greetTag = tsaComposer()(greet);
// Now it works both ways:
greet("Alice"); // ✓ Original still works
greetTag`Alice`; // ✓ Tagged template works too!
greetTag`${"Alice"}`; // ✓ With interpolation!The key insight: TSA Composer handles the template string parsing for you, so your function just receives the data it expects. No refactoring required!
Usage
Basic Usage (with default string joiner)
import tsaComposer from 'tsa-composer';
// Simple string transformation
const greet = tsaComposer()((name: string) => `Hello, ${name}!`);
const greeting = greet`Alice`;
// Result: "Hello, Alice!"
// Uppercase transformer
const shout = tsaComposer()((text: string) => text.toUpperCase());
const loud = shout`Hello ${"world"}!`;
// Result: "HELLO WORLD!"
// Async operations
const prompt = tsaComposer()(async (question: string) => {
// Your async logic here
return await getUserInput(question);
});
const answer = await prompt`What is your name? `;Using the Built-in String Joiner
import tsaComposer, { tsaStringJoiner } from 'tsa-composer';
// Explicitly use tsaStringJoiner (same as default)
const format = tsaComposer(tsaStringJoiner)((text: string) => {
return text.trim().replace(/\s+/g, ' ');
});
const clean = format`Hello ${"world"} and ${"everyone"}!`;
// Result: "Hello world and everyone!"Custom Parser Functions
import tsaComposer from 'tsa-composer';
// Custom parser that returns multiple arguments
const customParser = (
tsa: TemplateStringsArray,
...slots: number[]
): [number, number] => {
const sum = slots.reduce((a, b) => a + b, 0);
const avg = slots.length ? sum / slots.length : 0;
return [sum, avg];
};
const calculate = tsaComposer(customParser)((sum: number, avg: number) => {
return `Sum: ${sum}, Average: ${avg.toFixed(2)}`;
});
const result = calculate`Values: ${10}, ${20}, ${30}`;
// Result: "Sum: 60, Average: 20.00"Real-World Examples
// SQL query builder
const sql = tsaComposer()((query: string) => ({
query: query.trim(),
execute: () => executeQuery(query),
explain: () => explainQuery(query)
}));
const userQuery = sql`SELECT * FROM users WHERE id = ${"123"}`;
// userQuery.query: "SELECT * FROM users WHERE id = 123"
// userQuery.execute() - runs the query
// userQuery.explain() - shows query plan
// Logger with metadata
const log = tsaComposer()((message: string) => ({
message,
timestamp: new Date().toISOString(),
level: 'info',
write: () => console.log(message)
}));
const entry = log`User ${"Alice"} logged in`;
// entry.message: "User Alice logged in"
// entry.timestamp: "2025-10-06T..."
// entry.write() - outputs to console
// HTML sanitizer
const safe = tsaComposer()((html: string) => {
return html
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
});
const safeHtml = safe`<div>${"<script>alert('xss')</script>"}</div>`;
// Result: "<div><script>alert('xss')</script></div>"Wrapping Existing Functions (Zero Refactoring!)
Turn any existing function into a tagged template literal with zero changes:
// You already have these functions
function fetchUser(id: string) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function logError(message: string) {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
}
// Wrap them with tsaComposer - no changes to originals!
const getUser = tsaComposer()(fetchUser);
const checkEmail = tsaComposer()(validateEmail);
const error = tsaComposer()(logError);
// Now use them as tagged templates:
await getUser`123`;
// Same as: fetchUser("123")
checkEmail`[email protected]`;
// Same as: validateEmail("[email protected]")
error`Failed to load user ${"Alice"}`;
// Same as: logError("Failed to load user Alice")
// The original functions still work normally!
fetchUser("123");
validateEmail("[email protected]");
logError("Some error");Real-world benefit: Add template literal support to your entire API without touching existing code. Perfect for:
- Making existing utility functions work as tag functions
- Adding DSL-like syntax to legacy codebases
- Creating backward-compatible APIs
- Gradual migration to tagged template patterns
API## API
tsaComposer()
Creates a template literal tag function using the default tsaStringJoiner parser.
Signature:
tsaComposer(): <Fn extends (arg: string) => any>(
fn: Fn
) => (tsa: TemplateStringsArray, ...slots: string[]) => ReturnType<Fn>Returns:
- A function that takes a transformation function and returns a tagged template literal
Example:
const greet = tsaComposer()((name: string) => `Hello, ${name}!`);
const result = greet`World`;
// Result: "Hello, World!"tsaComposer(parse)
Creates a template literal tag function with a custom parser.
Signature:
tsaComposer<Parse extends (tsa: TemplateStringsArray, ...slots: any[]) => any[]>(
parse: Parse
): <Fn extends (...args: any[]) => any>(
fn: Fn
) => (tsa: TemplateStringsArray, ...slots: Parameters<Parse>[1][]) => ReturnType<Fn>Parameters:
parse- A function that takesTemplateStringsArrayand slot values, returns an array of arguments for the transformation function
Returns:
- A curried function that takes a transformation function and returns a tagged template literal
Example:
const customParser = (tsa: TemplateStringsArray, ...slots: number[]) => {
return [slots.reduce((a, b) => a + b, 0)];
};
const sum = tsaComposer(customParser)((total: number) => {
return `Total: ${total}`;
});
const result = sum`${10} + ${20} + ${30}`;
// Result: "Total: 60"tsaStringJoiner(tsa, ...slots)
Built-in parser that joins template strings and slots into a single string.
Signature:
tsaStringJoiner(
tsa: TemplateStringsArray,
...slots: string[]
): [string]Parameters:
tsa- The template strings array...slots- The interpolated values
Returns:
- An array containing the joined string
Example:
import { tsaStringJoiner } from 'tsa-composer';
const tsa = Object.assign(["Hello, ", "!"], { raw: ["Hello, ", "!"] });
const result = tsaStringJoiner(tsa, "World");
// Result: ["Hello, World!"]Advanced Usage
Custom Parsers for Complex Data
Create parsers that extract and transform data in sophisticated ways:
// Parser that extracts key-value pairs
const kvParser = (
tsa: TemplateStringsArray,
...slots: string[]
): [Map<string, string>] => {
const text = tsa.reduce((acc, s, i) => acc + s + (slots[i] ?? ""), "");
const map = new Map<string, string>();
const pairs = text.split(',').map(p => p.trim());
for (const pair of pairs) {
const [key, value] = pair.split(':').map(s => s.trim());
if (key && value) map.set(key, value);
}
return [map];
};
const config = tsaComposer(kvParser)((settings: Map<string, string>) => ({
get: (key: string) => settings.get(key),
has: (key: string) => settings.has(key),
all: () => Object.fromEntries(settings)
}));
const app = config`host: ${"localhost"}, port: ${"3000"}, env: ${"dev"}`;
// app.get('host') -> "localhost"
// app.all() -> { host: "localhost", port: "3000", env: "dev" }Composing Multiple Parsers
Build complex transformations by composing parsers:
// Number array parser
const numArrayParser = (
tsa: TemplateStringsArray,
...slots: number[]
): number[] => slots;
const stats = tsaComposer(numArrayParser)((...nums: number[]) => ({
sum: nums.reduce((a, b) => a + b, 0),
avg: nums.reduce((a, b) => a + b, 0) / nums.length,
min: Math.min(...nums),
max: Math.max(...nums),
count: nums.length
}));
const analysis = stats`${10} ${20} ${30} ${40} ${50}`;
// Result: { sum: 150, avg: 30, min: 10, max: 50, count: 5 }Type-Safe Transformations
Leverage TypeScript's type system for compile-time safety:
type User = { id: string; name: string; role: string };
const userParser = (
tsa: TemplateStringsArray,
...slots: User[]
): [User[]] => [slots];
const formatUsers = tsaComposer(userParser)((users: User[]) => {
return users
.map(u => `${u.name} (${u.role})`)
.join(', ');
});
const alice: User = { id: '1', name: 'Alice', role: 'Admin' };
const bob: User = { id: '2', name: 'Bob', role: 'User' };
const team = formatUsers`Team: ${alice}, ${bob}`;
// Result: "Team: Alice (Admin), Bob (User)"
// Type-safe: only User objects can be interpolatedDevelopment
To install dependencies:
bun installTo run tests:
bun testTo build the package:
bun run buildWhy Use TSA Composer?
- Type Safety: Full TypeScript support with proper type inference for inputs and outputs
- Curried API: Elegant composition pattern separating parsing from transformation
- Default Convenience: Built-in string joiner for common use cases
- Custom Parsers: Full control over how template strings are processed
- Flexibility: Transform template literals into any data type or structure
- Composability: Build complex transformations from simple building blocks
- Zero Dependencies: Minimal overhead with no external dependencies
- Tiny Footprint: Lightweight library that won't bloat your bundle
How It Works
TSA Composer uses a two-step curried approach:
- Parser Stage:
tsaComposer(parser)- Defines how to extract/transform the template string and its interpolated values into function arguments - Transform Stage:
(fn)- Defines what to do with those arguments
This separation allows for:
- Reusable parsers across different transformations
- Type-safe composition with full inference
- Clear separation of concerns between extraction and transformation
Example flow:
const parser = (tsa: TemplateStringsArray, ...slots: string[]) => [joined_string];
const transform = (text: string) => processedResult;
const tagged = tsaComposer(parser)(transform);
const result = tagged`template ${value} string`;License
MIT © snomiao
