Skip to content

Commit f27dc31

Browse files
authored
Add Template Literal Types for Case Conversions (#18)
* add template literal types * update docs
1 parent e4fc662 commit f27dc31

17 files changed

+1036
-20
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.8.0] - 2025-09-07
11+
12+
### Added
13+
14+
- **Template Literal Types for Case Conversions** - Compile-time type transformations
15+
- Advanced TypeScript template literal types for all 9 case conversion functions
16+
- Precise type inference for literal strings (e.g., `kebabCase("helloWorld")` returns type `"hello-world"`)
17+
- Function overloads preserve literal types while maintaining backward compatibility
18+
- Complex type-level string parsing with proper word boundary detection
19+
- Handles camelCase, PascalCase, ALLCAPS, consecutive digits, and special characters
20+
- Zero runtime cost - all transformations happen at compile time
21+
- Runtime strings still return regular `string` type for backward compatibility
22+
- Comprehensive type-level tests and real-world usage examples
23+
- Enhanced IDE support with autocomplete showing exact transformed strings
24+
- No impact on bundle size (remains at 6.01 kB)
25+
1026
## [0.7.0] - 2025-09-06
1127

1228
### Added

README.md

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ Ultra-lightweight string utilities with zero dependencies. Tree-shakeable, fully
1414
- 🚀 **Zero dependencies** - No bloat, just pure functions
1515
- 📦 **< 1KB per function** - Minimal bundle impact
1616
- 🌳 **Tree-shakeable** - Only import what you need
17-
- 💪 **Fully typed** - Complete TypeScript support with function overloads
17+
- 💪 **Fully typed** - Complete TypeScript support with function overloads and template literal types
1818
-**Fast performance** - 2-25x faster than lodash for many operations
1919
-**ESM & CJS** - Works everywhere
2020
- 🧪 **100% tested** - Reliable and production-ready
21-
- 🔒 **Type-safe** - Written in strict TypeScript with enhanced type inference
21+
- 🔒 **Type-safe** - Written in strict TypeScript with enhanced type inference and compile-time transformations
2222
- 📝 **Well documented** - JSDoc comments for all functions
2323

2424
## Installation
@@ -875,6 +875,79 @@ if (validated) {
875875
}
876876
```
877877

878+
### Template Literal Types (TypeScript)
879+
880+
Case conversion functions now provide precise type inference for literal strings at compile time. This feature enhances IDE support with exact type transformations while maintaining full backward compatibility.
881+
882+
```typescript
883+
import { camelCase, kebabCase, snakeCase } from "nano-string-utils";
884+
885+
// Literal strings get exact transformed types
886+
const endpoint = kebabCase("getUserProfile");
887+
// Type: "get-user-profile" (not just string!)
888+
889+
const column = snakeCase("firstName");
890+
// Type: "first_name"
891+
892+
const methodName = camelCase("fetch-user-data");
893+
// Type: "fetchUserData"
894+
895+
// Runtime strings still return regular string type
896+
const userInput: string = getUserInput();
897+
const result = camelCase(userInput);
898+
// Type: string (backward compatible)
899+
```
900+
901+
#### All Case Conversions Support Template Literals
902+
903+
```typescript
904+
camelCase("hello-world"); // Type: "helloWorld"
905+
kebabCase("helloWorld"); // Type: "hello-world"
906+
snakeCase("HelloWorld"); // Type: "hello_world"
907+
pascalCase("hello-world"); // Type: "HelloWorld"
908+
constantCase("helloWorld"); // Type: "HELLO_WORLD"
909+
dotCase("HelloWorld"); // Type: "hello.world"
910+
pathCase("helloWorld"); // Type: "hello/world"
911+
sentenceCase("hello-world"); // Type: "Hello world"
912+
titleCase("hello-world"); // Type: "Hello World"
913+
```
914+
915+
#### Type-Safe Configuration Objects
916+
917+
Transform configuration keys between naming conventions:
918+
919+
```typescript
920+
const config = {
921+
"api-base-url": "https://api.example.com",
922+
"max-retries": 3,
923+
} as const;
924+
925+
// Convert keys to camelCase at type level
926+
type ConfigCamelCase = {
927+
[K in keyof typeof config as CamelCase<K>]: (typeof config)[K];
928+
};
929+
// Type: { apiBaseUrl: string; maxRetries: number; }
930+
```
931+
932+
#### API Route Mapping
933+
934+
Create type-safe method names from API routes:
935+
936+
```typescript
937+
type ApiRoutes = "user-profile" | "user-settings" | "admin-panel";
938+
939+
type MethodNames = {
940+
[K in ApiRoutes as `fetch${PascalCase<K>}`]: () => Promise<void>;
941+
};
942+
// Creates: fetchUserProfile(), fetchUserSettings(), fetchAdminPanel()
943+
```
944+
945+
Benefits:
946+
-**Zero runtime cost** - All transformations happen at compile time
947+
-**Better IDE support** - Autocomplete shows exact transformed strings
948+
-**Type safety** - Catch typos and incorrect transformations during development
949+
-**Backward compatible** - Runtime strings work exactly as before
950+
878951
## Bundle Size
879952

880953
Each utility is optimized to be as small as possible:

examples/template-literal-types.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/**
2+
* Examples demonstrating template literal types for case conversions
3+
* These provide compile-time type transformations for better type safety
4+
*/
5+
6+
import {
7+
camelCase,
8+
kebabCase,
9+
snakeCase,
10+
pascalCase,
11+
constantCase,
12+
dotCase,
13+
pathCase,
14+
sentenceCase,
15+
titleCase,
16+
} from "../src/index.js";
17+
18+
import type {
19+
CamelCase,
20+
KebabCase,
21+
SnakeCase,
22+
PascalCase,
23+
ConstantCase,
24+
DotCase,
25+
PathCase,
26+
SentenceCase,
27+
TitleCase,
28+
} from "../src/types/string-literals.js";
29+
30+
// ============================================
31+
// 1. Basic Usage - Literal strings get literal types
32+
// ============================================
33+
34+
// When you pass a literal string, you get the exact transformed type back
35+
const apiEndpoint = kebabCase("getUserProfile");
36+
// Type: "get-user-profile" (not just string!)
37+
38+
const dbColumn = snakeCase("firstName");
39+
// Type: "first_name"
40+
41+
const componentName = pascalCase("user-avatar");
42+
// Type: "UserAvatar"
43+
44+
const envVar = constantCase("apiKey");
45+
// Type: "API_KEY"
46+
47+
// ============================================
48+
// 2. Type-safe API Routes
49+
// ============================================
50+
51+
// Define your routes with literal types
52+
type ApiRoutes = "user-profile" | "user-settings" | "admin-panel";
53+
54+
// Convert to camelCase for JavaScript method names
55+
type MethodNames = {
56+
[K in ApiRoutes as `fetch${PascalCase<K>}`]: () => Promise<void>;
57+
};
58+
59+
// This creates an interface with:
60+
// fetchUserProfile(): Promise<void>
61+
// fetchUserSettings(): Promise<void>
62+
// fetchAdminPanel(): Promise<void>
63+
64+
// ============================================
65+
// 3. Configuration Objects
66+
// ============================================
67+
68+
// Convert between different naming conventions in configs
69+
const config = {
70+
"api-base-url": "https://api.example.com",
71+
"max-retries": 3,
72+
"timeout-ms": 5000,
73+
} as const;
74+
75+
// Transform keys to camelCase for JavaScript usage
76+
type ConfigCamelCase = {
77+
[K in keyof typeof config as CamelCase<K>]: (typeof config)[K];
78+
};
79+
80+
// Now you have type-safe camelCase keys:
81+
// apiBaseUrl: "https://api.example.com"
82+
// maxRetries: 3
83+
// timeoutMs: 5000
84+
85+
// ============================================
86+
// 4. Database Schema Mapping
87+
// ============================================
88+
89+
// Map JavaScript property names to database columns
90+
class User {
91+
firstName: string = "";
92+
lastName: string = "";
93+
emailAddress: string = "";
94+
95+
// Get the database column name with compile-time safety
96+
getColumn<T extends keyof this & string>(prop: T): SnakeCase<T> {
97+
return snakeCase(prop) as SnakeCase<T>;
98+
}
99+
}
100+
101+
const user = new User();
102+
const column = user.getColumn("firstName");
103+
// Type: "first_name"
104+
105+
// ============================================
106+
// 5. Event Handler Names
107+
// ============================================
108+
109+
// Convert event names to handler method names
110+
type EventName = "user-clicked" | "form-submitted" | "data-loaded";
111+
112+
type HandlerName<T extends EventName> = `on${PascalCase<T>}`;
113+
114+
function createHandler<T extends EventName>(event: T): HandlerName<T> {
115+
return `on${pascalCase(event)}` as HandlerName<T>;
116+
}
117+
118+
const clickHandler = createHandler("user-clicked");
119+
// Type: "onUserClicked"
120+
121+
// ============================================
122+
// 6. CSS Class Names
123+
// ============================================
124+
125+
// Convert component names to BEM-style CSS classes
126+
type ComponentName = "UserProfile" | "NavigationMenu" | "SearchBar";
127+
128+
function getCssClass<T extends ComponentName>(component: T): KebabCase<T> {
129+
return kebabCase(component) as KebabCase<T>;
130+
}
131+
132+
const cssClass = getCssClass("UserProfile");
133+
// Type: "user-profile"
134+
135+
// ============================================
136+
// 7. Backward Compatibility
137+
// ============================================
138+
139+
// Runtime strings still work exactly as before
140+
function processUserInput(input: string): string {
141+
// Returns regular string type for runtime values
142+
return camelCase(input);
143+
}
144+
145+
const userInput = "some-runtime-value";
146+
const result = camelCase(userInput);
147+
// Type: string (not a literal type)
148+
149+
// ============================================
150+
// 8. Chaining Transformations
151+
// ============================================
152+
153+
// Chain multiple transformations with type safety
154+
const original = "hello-world" as const;
155+
156+
const camel = camelCase(original);
157+
// Type: "helloWorld"
158+
159+
const pascal = pascalCase(camel);
160+
// Type: "HelloWorld"
161+
162+
const constant = constantCase(pascal);
163+
// Type: "HELLO_WORLD"
164+
165+
const sentence = sentenceCase(constant);
166+
// Type: "Hello world"
167+
168+
// ============================================
169+
// 9. Template Literal Composition
170+
// ============================================
171+
172+
const prefix = "get" as const;
173+
const entity = "user-profile" as const;
174+
const methodName = camelCase(`${prefix}-${entity}`);
175+
// Type: "getUserProfile"
176+
177+
// ============================================
178+
// 10. Type Guards with Literal Types
179+
// ============================================
180+
181+
type KebabString = `${string}-${string}`;
182+
183+
function isKebabCase(str: string): str is KebabString {
184+
return str.includes("-") && str === str.toLowerCase();
185+
}
186+
187+
function processKebab(str: string) {
188+
if (isKebabCase(str)) {
189+
// TypeScript knows str matches kebab pattern
190+
const camelVersion = camelCase(str);
191+
// Gets more precise type inference
192+
return camelVersion;
193+
}
194+
return str;
195+
}
196+
197+
// ============================================
198+
// Export for demonstration
199+
// ============================================
200+
201+
export {
202+
apiEndpoint,
203+
dbColumn,
204+
componentName,
205+
envVar,
206+
clickHandler,
207+
cssClass,
208+
methodName,
209+
};

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zheruel/nano-string-utils",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"exports": "./src/index.ts",
55
"publish": {
66
"include": ["src/**/*.ts", "README.md", "LICENSE", "CHANGELOG.md"],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nano-string-utils",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"description": "Ultra-lightweight string utilities with zero dependencies",
55
"type": "module",
66
"main": "./dist/index.cjs",

src/camelCase.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { words } from "./words.js";
2+
import type { CamelCase } from "./types/string-literals.js";
23

34
/**
45
* Converts a string to camelCase
@@ -8,8 +9,13 @@ import { words } from "./words.js";
89
* camelCase('hello world') // 'helloWorld'
910
* camelCase('hello-world') // 'helloWorld'
1011
* camelCase('hello_world') // 'helloWorld'
12+
*
13+
* // With template literal types
14+
* const result = camelCase('hello-world'); // type: "helloWorld"
1115
*/
12-
export const camelCase = (str: string): string => {
16+
export function camelCase<T extends string>(str: T): CamelCase<T>;
17+
export function camelCase(str: string): string;
18+
export function camelCase(str: string): string {
1319
const wordList = words(str);
1420

1521
if (wordList.length === 0) return "";
@@ -22,4 +28,4 @@ export const camelCase = (str: string): string => {
2228
return lower.charAt(0).toUpperCase() + lower.slice(1);
2329
})
2430
.join("");
25-
};
31+
}

src/constantCase.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ConstantCase } from "./types/string-literals.js";
2+
13
// Pre-compiled regex patterns for better performance
24
const CONST_ACRONYM = /([A-Z]+)([A-Z][a-z])/g;
35
const CONST_LOWERCASE_UPPER = /([a-z0-9])([A-Z])/g;
@@ -15,8 +17,13 @@ const CONST_SPLIT = /\s+/;
1517
* constantCase('helloWorld') // 'HELLO_WORLD'
1618
* constantCase('hello-world') // 'HELLO_WORLD'
1719
* constantCase('__hello__world__') // 'HELLO_WORLD'
20+
*
21+
* // With template literal types
22+
* const result = constantCase('helloWorld'); // type: "HELLO_WORLD"
1823
*/
19-
export const constantCase = (str: string): string => {
24+
export function constantCase<T extends string>(str: T): ConstantCase<T>;
25+
export function constantCase(str: string): string;
26+
export function constantCase(str: string): string {
2027
if (!str) return str;
2128

2229
// Handle camelCase/PascalCase by adding spaces before capitals
@@ -43,4 +50,4 @@ export const constantCase = (str: string): string => {
4350
.join("_");
4451

4552
return result;
46-
};
53+
}

0 commit comments

Comments
 (0)