A lightweight, flexible TypeScript utility for composing template string tag functions with custom parsers and transformations.
npm install tsa-composerA lightweight, flexible TypeScript utility for composing template string tag functions with custom parsers and transformations.
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
``bashUsing npm
npm install tsa-composer
The 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:
`typescript
// 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:
`typescript
// 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
$3
`typescript
import tsaComposer from 'tsa-composer';// Simple string transformation
const greet = tsaComposer()((name: string) =>
Hello, ${name}!);
const greeting = greetAlice;
// 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? ;
`$3
`typescript
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!"
`$3
`typescript
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"
`$3
`typescript
// 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(/ .replace(/>/g, '>')
.replace(/"/g, '"');
});
const safeHtml = safe
;
// Result: "<div><script>alert('xss')</script></div>"
`$3
Turn any existing function into a tagged template literal with zero changes:
`typescript
// 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
user@example.com;
// Same as: validateEmail("user@example.com")error
Failed to load user ${"Alice"};
// Same as: logError("Failed to load user Alice")// The original functions still work normally!
fetchUser("123");
validateEmail("user@example.com");
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
$3
Creates a template literal tag function using the default
tsaStringJoiner parser.Signature:
`typescript
tsaComposer(): any>(
fn: Fn
) => (tsa: TemplateStringsArray, ...slots: string[]) => ReturnType
`Returns:
- A function that takes a transformation function and returns a tagged template literal
Example:
`typescript
const greet = tsaComposer()((name: string) => Hello, ${name}!);
const result = greetWorld;
// Result: "Hello, World!"
`$3
Creates a template literal tag function with a custom parser.
Signature:
`typescript
tsaComposer any[]>(
parse: Parse
): any>(
fn: Fn
) => (tsa: TemplateStringsArray, ...slots: Parameters[1][]) => ReturnType
`Parameters:
-
parse - A function that takes TemplateStringsArray and slot values, returns an array of arguments for the transformation functionReturns:
- A curried function that takes a transformation function and returns a tagged template literal
Example:
`typescript
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"
`$3
Built-in parser that joins template strings and slots into a single string.
Signature:
`typescript
tsaStringJoiner(
tsa: TemplateStringsArray,
...slots: string[]
): [string]
`Parameters:
-
tsa - The template strings array
- ...slots - The interpolated valuesReturns:
- An array containing the joined string
Example:
`typescript
import { tsaStringJoiner } from 'tsa-composer';const tsa = Object.assign(["Hello, ", "!"], { raw: ["Hello, ", "!"] });
const result = tsaStringJoiner(tsa, "World");
// Result: ["Hello, World!"]
`Advanced Usage
$3
Create parsers that extract and transform data in sophisticated ways:
`typescript
// Parser that extracts key-value pairs
const kvParser = (
tsa: TemplateStringsArray,
...slots: string[]
): [Map] => {
const text = tsa.reduce((acc, s, i) => acc + s + (slots[i] ?? ""), "");
const map = new Map();
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) => ({
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" }
`$3
Build complex transformations by composing parsers:
`typescript
// 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 }
`$3
Leverage TypeScript's type system for compile-time safety:
`typescript
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 interpolated
`Development
To install dependencies:
`bash
bun install
`To run tests:
`bash
bun test
`To build the package:
`bash
bun run build
`Why 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:
1. Parser Stage:
tsaComposer(parser) - Defines how to extract/transform the template string and its interpolated values into function arguments
2. Transform Stage: (fn) - Defines what to do with those argumentsThis separation allows for:
- Reusable parsers across different transformations
- Type-safe composition with full inference
- Clear separation of concerns between extraction and transformation
Example flow:
`typescript
const parser = (tsa: TemplateStringsArray, ...slots: string[]) => [joined_string];
const transform = (text: string) => processedResult;
const tagged = tsaComposer(parser)(transform);
const result = taggedtemplate ${value} string;
``MIT © snomiao