├── .all-contributorsrc ├── .github └── workflows │ ├── ci.yml │ └── codecov.yml ├── .gitignore ├── .wispbit └── rules │ ├── comprehensive-documentation.md │ ├── comprehensive-testing-standards.md │ ├── consistent-naming-conventions.md │ ├── documentation-examples-accuracy.md │ ├── edge-case-handling.md │ ├── object-key-transformation-pattern.md │ ├── project-configuration-standards.md │ └── type-safety-dual-implementation.md ├── LICENSE ├── README.md ├── biome.json ├── codecov.yml ├── docs └── string-ts-banner.png ├── package.json ├── pnpm-lock.yaml ├── scripts └── generate-entrypoints.mts ├── src ├── index.ts ├── internal │ ├── fixtures.ts │ ├── internals.test.ts │ ├── internals.ts │ ├── literals.test.ts │ ├── literals.ts │ ├── math.test.ts │ ├── math.ts │ └── types.d.ts ├── native │ ├── char-at.test.ts │ ├── char-at.ts │ ├── concat.test.ts │ ├── concat.ts │ ├── ends-with.test.ts │ ├── ends-with.ts │ ├── includes.test.ts │ ├── includes.ts │ ├── index.d.ts │ ├── join.test.ts │ ├── join.ts │ ├── length.test.ts │ ├── length.ts │ ├── native-overrides.test.ts │ ├── pad-end.test.ts │ ├── pad-end.ts │ ├── pad-start.test.ts │ ├── pad-start.ts │ ├── repeat.test.ts │ ├── repeat.ts │ ├── replace-all.test.ts │ ├── replace-all.ts │ ├── replace.test.ts │ ├── replace.ts │ ├── slice.test.ts │ ├── slice.ts │ ├── split.test.ts │ ├── split.ts │ ├── starts-with.test.ts │ ├── starts-with.ts │ ├── to-lower-case.test.ts │ ├── to-lower-case.ts │ ├── to-upper-case.test.ts │ ├── to-upper-case.ts │ ├── trim-end.test.ts │ ├── trim-end.ts │ ├── trim-start.test.ts │ ├── trim-start.ts │ ├── trim.test.ts │ └── trim.ts └── utils │ ├── characters │ ├── apostrophe.ts │ ├── letters.test.ts │ ├── letters.ts │ ├── numbers.test.ts │ ├── numbers.ts │ ├── separators.test.ts │ ├── separators.ts │ ├── special.test.ts │ └── special.ts │ ├── object-keys │ ├── camel-keys.test.ts │ ├── camel-keys.ts │ ├── constant-keys.test.ts │ ├── constant-keys.ts │ ├── deep-camel-keys.test.ts │ ├── deep-camel-keys.ts │ ├── deep-constant-keys.test.ts │ ├── deep-constant-keys.ts │ ├── deep-delimiter-keys.test.ts │ ├── deep-delimiter-keys.ts │ ├── deep-kebab-keys.test.ts │ ├── deep-kebab-keys.ts │ ├── deep-pascal-keys.test.ts │ ├── deep-pascal-keys.ts │ ├── deep-snake-keys.test.ts │ ├── deep-snake-keys.ts │ ├── deep-transform-keys.test.ts │ ├── deep-transform-keys.ts │ ├── delimiter-keys.test.ts │ ├── delimiter-keys.ts │ ├── kebab-keys.test.ts │ ├── kebab-keys.ts │ ├── pascal-keys.test.ts │ ├── pascal-keys.ts │ ├── replace-keys.test.ts │ ├── replace-keys.ts │ ├── snake-keys.test.ts │ ├── snake-keys.ts │ ├── transform-keys.test.ts │ └── transform-keys.ts │ ├── reverse.test.ts │ ├── reverse.ts │ ├── truncate.test.ts │ ├── truncate.ts │ ├── word-case │ ├── camel-case.test.ts │ ├── camel-case.ts │ ├── capitalize.test.ts │ ├── capitalize.ts │ ├── constant-case.test.ts │ ├── constant-case.ts │ ├── delimiter-case.test.ts │ ├── delimiter-case.ts │ ├── kebab-case.test.ts │ ├── kebab-case.ts │ ├── lower-case.test.ts │ ├── lower-case.ts │ ├── pascal-case.test.ts │ ├── pascal-case.ts │ ├── snake-case.test.ts │ ├── snake-case.ts │ ├── title-case.test.ts │ ├── title-case.ts │ ├── uncapitalize.test.ts │ ├── uncapitalize.ts │ ├── upper-case.test.ts │ └── upper-case.ts │ ├── words.test.ts │ └── words.ts ├── tsconfig.dist.json ├── tsconfig.json └── vitest.config.ts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["README.md"], 3 | "imageSize": 100, 4 | "commit": false, 5 | "commitType": "docs", 6 | "commitConvention": "angular", 7 | "contributors": [ 8 | { 9 | "login": "gustavoguichard", 10 | "name": "Guga Guichard", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/566971?v=4", 12 | "profile": "https://github.com/gustavoguichard", 13 | "contributions": [ 14 | "code", 15 | "projectManagement", 16 | "promotion", 17 | "maintenance", 18 | "doc", 19 | "bug", 20 | "infra", 21 | "question", 22 | "research", 23 | "review", 24 | "ideas", 25 | "example" 26 | ] 27 | }, 28 | { 29 | "login": "jly36963", 30 | "name": "Landon Yarrington", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/33426811?v=4", 32 | "profile": "https://github.com/jly36963", 33 | "contributions": [ 34 | "code", 35 | "maintenance", 36 | "doc", 37 | "review", 38 | "ideas", 39 | "example", 40 | "question", 41 | "bug" 42 | ] 43 | }, 44 | { 45 | "login": "p9f", 46 | "name": "Guillaume", 47 | "avatar_url": "https://avatars.githubusercontent.com/u/20539361?v=4", 48 | "profile": "https://github.com/p9f", 49 | "contributions": [ 50 | "code", 51 | "maintenance", 52 | "doc", 53 | "bug", 54 | "infra", 55 | "question", 56 | "ideas" 57 | ] 58 | }, 59 | { 60 | "login": "mattpocock", 61 | "name": "Matt Pocock", 62 | "avatar_url": "https://avatars.githubusercontent.com/u/28293365?v=4", 63 | "profile": "https://totaltypescript.com", 64 | "contributions": ["doc", "code", "promotion"] 65 | }, 66 | { 67 | "login": "iamandrewluca", 68 | "name": "Andrew Luca", 69 | "avatar_url": "https://avatars.githubusercontent.com/u/1881266?v=4", 70 | "profile": "https://luca.md", 71 | "contributions": ["doc", "promotion"] 72 | }, 73 | { 74 | "login": "mjuksel", 75 | "name": "Mjuksel", 76 | "avatar_url": "https://avatars.githubusercontent.com/u/10691584?v=4", 77 | "profile": "https://github.com/mjuksel", 78 | "contributions": ["code", "ideas"] 79 | }, 80 | { 81 | "login": "hverlin", 82 | "name": "hverlin", 83 | "avatar_url": "https://avatars.githubusercontent.com/u/9151470?v=4", 84 | "profile": "https://huguesverlin.fr", 85 | "contributions": ["code"] 86 | } 87 | ], 88 | "contributorsPerLine": 7, 89 | "skipCi": true, 90 | "repoType": "github", 91 | "repoHost": "https://github.com", 92 | "projectName": "string-ts", 93 | "projectOwner": "gustavoguichard" 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | 6 | jobs: 7 | typecheck-outputs: 8 | name: 🚚 Typecheck Outputs / ${{ matrix.typescript-version }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | typescript-version: 14 | - '~5.7.0' 15 | - '~5.6.0' 16 | - '~5.5.0' 17 | - '~5.4.0' 18 | - '~5.3.0' 19 | - '~5.2.0' 20 | - '~5.1.0' 21 | # We use features that were added in v5.0 of typescript, so that is 22 | # the lowest we can go here. This also means this is the lowest 23 | # version we support. When this value changes in the future it needs 24 | # to be communicated to the users. 25 | - '~5.0.0' 26 | 27 | steps: 28 | - name: ⬇️ Checkout repo 29 | uses: actions/checkout@v4 30 | 31 | - name: 📦 Manually Install pnpm 32 | run: npm install -g pnpm@10 33 | 34 | - name: 🔧 Ensure TSX is available 35 | run: pnpm add -D tsx 36 | 37 | - name: 🪡 Install Node.js 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: '18' 41 | cache: 'pnpm' 42 | 43 | - name: 📦 Install Dependencies 44 | run: pnpm install --frozen-lockfile 45 | 46 | # Order is important here, we build with the typescript version defined 47 | # in package.json, before we overrite it for the tests. 48 | - name: 🏗️ Build 49 | run: pnpm run build 50 | 51 | - name: 📘 Install Typescript 52 | run: pnpm add -D typescript@${{ matrix.typescript-version }} 53 | 54 | - name: 🔎 Type check 55 | run: pnpm run tsc:dist 56 | 57 | 58 | test: 59 | name: Run tests 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - name: 🛑 Cancel Previous Runs 64 | uses: styfle/cancel-workflow-action@0.12.1 65 | 66 | - name: ⬇️ Checkout repo 67 | uses: actions/checkout@v4 68 | 69 | - name: 📦 Manually Install pnpm 70 | run: npm install -g pnpm@10 71 | 72 | - name: 🪡 Install Node.js 73 | uses: actions/setup-node@v4 74 | with: 75 | node-version: '18' 76 | cache: 'pnpm' 77 | 78 | - name: 📦 Install Dependencies 79 | run: pnpm install --frozen-lockfile 80 | 81 | - name: ⚡ Run tests 82 | run: | 83 | pnpm run test 84 | - name: 🚦 Lint 85 | run: | 86 | pnpm run lint 87 | - name: 🧙🏿‍♂️ TSC 88 | run: | 89 | pnpm run tsc 90 | - name: 📥 Generate npm package 91 | run: | 92 | pnpm run build 93 | - name: ✅ Verify native entrypoint files 94 | run: | 95 | test -f dist/native.d.ts && test -f dist/native.js && test -f dist/native.mjs 96 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | codecov: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cancel Previous Runs 13 | uses: styfle/cancel-workflow-action@0.12.1 14 | 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: 📦 Manually Install pnpm 19 | run: npm install -g pnpm@10 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 18 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: Run tests 31 | run: pnpm run test -- --coverage 32 | 33 | - name: Upload coverage to github actions artifacts 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: code-coverage 37 | path: coverage/ 38 | 39 | - name: Upload coverage to codecov 40 | uses: codecov/codecov-action@v5 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bun.lockb 2 | .vscode/ 3 | node_modules 4 | dist/ 5 | npm 6 | tsc/ 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.wispbit/rules/comprehensive-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.ts, *.tsx 3 | --- 4 | 5 | # Comprehensive Documentation Standards 6 | 7 | All exported functions and types must be thoroughly documented to ensure usability and maintainability. 8 | 9 | ## Requirements 10 | 11 | 1. Include JSDoc comments for all exported functions and types 12 | 2. Always include example usage in function documentation 13 | 3. Document parameters with descriptive names and types 14 | 4. Document return values 15 | 5. Include edge case handling in documentation when relevant 16 | 17 | ## Examples 18 | 19 | ### Good ✅ 20 | ```typescript 21 | /** 22 | * A strongly-typed version of `String.prototype.charAt`. 23 | * @param str the string to get the character from. 24 | * @param index the index of the character. 25 | * @returns the character in both type level and runtime. 26 | * @example charAt('hello world', 6) // 'w' 27 | */ 28 | export function charAt( 29 | str: T, 30 | index: I 31 | ): CharAt { 32 | return str.charAt(index) as CharAt 33 | } 34 | ``` 35 | 36 | ### Bad ❌ 37 | ```typescript 38 | // Missing documentation 39 | export function charAt( 40 | str: T, 41 | index: I 42 | ): CharAt { 43 | return str.charAt(index) as CharAt 44 | } 45 | 46 | // Or incomplete documentation 47 | /** 48 | * Gets a character from a string. 49 | */ 50 | export function charAt(str, index) { 51 | return str.charAt(index) 52 | } 53 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/comprehensive-testing-standards.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.test.ts 3 | --- 4 | 5 | # Comprehensive Testing Standards 6 | 7 | All string utility functions must have thorough tests covering both runtime behavior and type-level correctness. 8 | 9 | ## Testing Requirements 10 | 11 | 1. Test both runtime functionality and type-level behavior 12 | 2. Include tests for edge cases (empty strings, negative indices, non-literal types) 13 | 3. Organize tests in a structured, consistent manner 14 | 4. Extract reusable test values to variables 15 | 16 | ## Test Organization 17 | 18 | 1. Group type tests within namespaces that reflect the functionality being tested 19 | 2. Group runtime tests within describe blocks that match the structure of type tests 20 | 3. Organize test suites alphabetically for better navigation 21 | 22 | ## Examples 23 | 24 | ### Good ✅ 25 | 26 | ```typescript 27 | // Type-level tests in a namespace 28 | namespace TruncateTests { 29 | type test1 = Expect, 'Hello,...'>> 30 | type test2 = Expect, 'Hello'>> 31 | // Edge case: negative length 32 | type test3 = Expect, '...'>> 33 | } 34 | 35 | // Runtime tests in a matching describe block 36 | describe('truncate', () => { 37 | // Extract reusable test values 38 | const longText = 'Hello, world'; 39 | const shortText = 'Hello'; 40 | 41 | test('truncates a string that exceeds target length', () => { 42 | expect(truncate(longText, 9, '...')).toEqual('Hello,...'); 43 | }); 44 | 45 | test('does not truncate a string shorter than target length', () => { 46 | expect(truncate(shortText, 10, '...')).toEqual('Hello'); 47 | }); 48 | 49 | // Edge case test 50 | test('handles negative length by returning just the omission', () => { 51 | expect(truncate(longText, -1, '...')).toEqual('...'); 52 | }); 53 | }); 54 | ``` 55 | 56 | ### Bad ❌ 57 | 58 | ```typescript 59 | // Disorganized testing 60 | describe('string functions', () => { 61 | test('truncate works', () => { 62 | expect(truncate('Hello, world', 9, '...')).toEqual('Hello,...'); 63 | expect(truncate('Hello', 10, '...')).toEqual('Hello'); 64 | // Missing edge case tests 65 | }); 66 | 67 | // Missing type tests 68 | }); 69 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/consistent-naming-conventions.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.ts, *.tsx 3 | --- 4 | 5 | # Consistent Naming Conventions 6 | 7 | Maintain consistent naming patterns throughout the codebase to improve readability and predictability. 8 | 9 | ## Naming Guidelines 10 | 11 | 1. **Functions**: Use `lowerCamelCase` for all function names (e.g., `camelCase`, `trimStart`) 12 | 2. **Types**: Use `PascalCase` for all type definitions (e.g., `CamelCase`, `TrimStart`) 13 | 3. **Files**: Use `kebab-case` for all filenames (e.g., `camel-case.ts`, `trim-start.ts`) 14 | 4. **Case Transformations**: Prefer direct naming (e.g., `camelCase`) over "to" prefix (e.g., `toCamelCase`) 15 | 16 | ## Deprecated Functions 17 | 18 | For backward compatibility, older naming patterns should be preserved but marked as deprecated: 19 | 20 | ```typescript 21 | /** 22 | * @deprecated 23 | * Use `camelCase` instead. 24 | */ 25 | export const toCamelCase = camelCase 26 | ``` 27 | 28 | ## Examples 29 | 30 | ### Good ✅ 31 | ```typescript 32 | // Type definition in PascalCase 33 | export type KebabCase = Lowercase< 34 | DelimiterCase, '-'> 35 | > 36 | 37 | // Function in lowerCamelCase 38 | export function kebabCase(str: T): KebabCase { 39 | return toLowerCase(delimiterCase(removeApostrophe(str), '-')) 40 | } 41 | 42 | // Deprecated function with notice 43 | /** 44 | * @deprecated 45 | * Use `kebabCase` instead. 46 | */ 47 | export const toKebabCase = kebabCase 48 | ``` 49 | 50 | ### Bad ❌ 51 | ```typescript 52 | // Inconsistent naming 53 | export type kebab_case = Lowercase< 54 | DelimiterCase, '-'> 55 | > 56 | 57 | export function KebabCase(str: T) { 58 | return toLowerCase(delimiterCase(removeApostrophe(str), '-')) 59 | } 60 | 61 | // Missing deprecation notice 62 | export const toKebabCase = kebabCase 63 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/documentation-examples-accuracy.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.md 3 | --- 4 | 5 | # Documentation Examples Accuracy 6 | 7 | All code examples in documentation (README files, comments, JSDoc) must be accurate and demonstrate the actual behavior of the functions. 8 | 9 | ## Requirements 10 | 11 | 1. Examples must produce the exact outputs shown in comments 12 | 2. Examples should be simple and focused on demonstrating the feature 13 | 3. Include proper syntax highlighting for code blocks 14 | 4. Maintain alphabetical ordering of sections in documentation 15 | 5. Include badges for npm version, package size, and other project metrics when appropriate 16 | 17 | ## Examples 18 | 19 | ### Good ✅ 20 | 21 | ```markdown 22 | [![NPM](https://img.shields.io/npm/v/string-ts)](https://www.npmjs.org/package/string-ts) 23 | ![Library size](https://img.shields.io/bundlephobia/minzip/string-ts) 24 | 25 | # string-ts 26 | 27 | Strongly-typed string functions for all! 28 | 29 | ## Usage 30 | 31 | ```typescript 32 | import { camelCase, kebabCase } from 'string-ts'; 33 | 34 | // camelCase example 35 | const str1 = 'hello-world'; 36 | const result1 = camelCase(str1); 37 | // ^ 'helloWorld' 38 | 39 | // kebabCase example 40 | const str2 = 'helloWorld'; 41 | const result2 = kebabCase(str2); 42 | // ^ 'hello-world' 43 | ``` 44 | ``` 45 | 46 | ### Bad ❌ 47 | 48 | ```markdown 49 | # string-ts 50 | 51 | Strongly-typed string functions. 52 | 53 | ## Usage 54 | 55 | ```typescript 56 | // Incorrect output 57 | const str = 'hello-world'; 58 | const result = camelCase(str); 59 | // ^ 'HelloWorld' // WRONG! camelCase would produce 'helloWorld' 60 | 61 | // Inconsistent style 62 | const myString = 'hello-world'; 63 | const output = camelCase(myString); 64 | ``` 65 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/edge-case-handling.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.ts, *.tsx 3 | --- 4 | 5 | # Edge Case Handling Requirements 6 | 7 | String manipulation functions must properly handle edge cases to ensure consistent behavior. Always test and account for edge cases in both type-level and runtime implementations. 8 | 9 | ## Critical Edge Cases 10 | 11 | 1. Empty strings 12 | 2. Empty delimiters/separators 13 | 3. Negative indexes for position-based operations 14 | 4. Cases where target length is shorter than input string 15 | 5. Non-literal types (e.g., `string` instead of string literals) 16 | 17 | ## Examples 18 | 19 | ### Good ✅ 20 | 21 | ```typescript 22 | // Properly handling empty strings and negative cases in types 23 | type TrimStart = T extends ` ${infer rest}` 24 | ? TrimStart 25 | : T 26 | 27 | // Handling negative indexes in runtime 28 | function slice>( 29 | str: T, 30 | start: S = 0 as S, 31 | end: E = str.length as E 32 | ): Slice { 33 | return str.slice(start, end) as Slice 34 | } 35 | 36 | // Handling non-literal types 37 | type CharAt = All< 38 | [IsStringLiteral, IsNumberLiteral] 39 | > extends true 40 | ? Split[index] 41 | : string 42 | ``` 43 | 44 | ### Bad ❌ 45 | 46 | ```typescript 47 | // Not handling empty string inputs 48 | type Split< 49 | T, 50 | delimiter extends string, 51 | > = T extends `${infer first}${delimiter}${infer rest}` 52 | ? [first, ...Split] 53 | : [T] // This could result in [''] for empty strings, which might be unexpected 54 | 55 | // Not handling negative indexes 56 | function charAt( 57 | str: T, 58 | index: I 59 | ): CharAt { 60 | // No handling for negative indexes 61 | return str.charAt(index) as CharAt 62 | } 63 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/object-key-transformation-pattern.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.ts, *.tsx 3 | --- 4 | 5 | # Object Key Transformation Pattern 6 | 7 | When implementing object key transformations, follow a consistent pattern and clearly distinguish between shallow and deep (recursive) transformations. 8 | 9 | ## Requirements 10 | 11 | 1. **Shallow vs Deep**: Clearly identify functions that transform only top-level keys versus those that recursively transform nested objects 12 | 2. **Naming Convention**: Use `camelKeys` for shallow transforms and `deepCamelKeys` for recursive transforms 13 | 3. **Type Definitions**: Provide corresponding type definitions (`CamelKeys` and `DeepCamelKeys`) 14 | 4. **Documentation**: Clearly document the transformation depth in JSDoc comments 15 | 16 | ## Examples 17 | 18 | ### Good ✅ 19 | 20 | ```typescript 21 | /** 22 | * A strongly typed function that shallowly transforms the keys of an object to camelCase. 23 | * The transformation is done both at runtime and type level. 24 | */ 25 | function camelKeys(obj: T): CamelKeys { 26 | return transformKeys(obj, camelCase) as CamelKeys 27 | } 28 | 29 | /** 30 | * A strongly typed function that recursively transforms the keys of an object to camelCase. 31 | * The transformation is done both at runtime and type level. 32 | */ 33 | function deepCamelKeys(obj: T): DeepCamelKeys { 34 | return deepTransformKeys(obj, camelCase) as DeepCamelKeys 35 | } 36 | ``` 37 | 38 | ### Bad ❌ 39 | 40 | ```typescript 41 | /** 42 | * Transforms the keys of an object to camelCase. 43 | * [No indication if shallow or deep] 44 | */ 45 | function camelKeys(obj: T): CamelKeys { 46 | return transformKeys(obj, camelCase) as CamelKeys 47 | } 48 | 49 | /** 50 | * A strongly typed function that recursively transforms the keys of an object to camelCase. 51 | * [Incorrect name - should be deepCamelKeys for recursive] 52 | */ 53 | function camelKeysRecursive(obj: T): DeepCamelKeys { 54 | return deepTransformKeys(obj, camelCase) as DeepCamelKeys 55 | } 56 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/project-configuration-standards.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: package.json, .github/** 3 | --- 4 | 5 | # Project Configuration Standards 6 | 7 | Maintain consistent project configuration files for dependency management, CI workflows, and contributor recognition. 8 | 9 | ## Requirements 10 | 11 | 1. **Package.json** 12 | - Add `"sideEffects": false` to enable tree-shaking 13 | - Use consistent versioning syntax for dependencies (e.g., caret `^` or tilde `~`) 14 | - Update related packages together 15 | 16 | 2. **Dependabot** 17 | - Maintain a proper `.github/dependabot.yml` configuration 18 | - Configure all used package ecosystems (npm, github-actions) 19 | - Use appropriate update schedule and settings 20 | 21 | 3. **CI Workflow** 22 | - CI workflows should run on both push to main branch and pull requests 23 | - Include tests, linting, type checking, and build steps 24 | - Test against all supported TypeScript versions 25 | 26 | 4. **All Contributors** 27 | - Use the All Contributors spec to recognize all types of contributions 28 | - Maintain the `.all-contributorsrc` file 29 | - Include the contributors section in the README 30 | 31 | ## Examples 32 | 33 | ### Good ✅ 34 | 35 | ```json 36 | // package.json 37 | { 38 | "name": "string-ts", 39 | "version": "1.0.0", 40 | "sideEffects": false, 41 | "devDependencies": { 42 | "@vitest/coverage-v8": "^1.5.2", 43 | "typescript": "^5.4.5" 44 | } 45 | } 46 | ``` 47 | 48 | ```yaml 49 | # .github/dependabot.yml 50 | version: 2 51 | updates: 52 | - package-ecosystem: "npm" 53 | directory: "/" 54 | schedule: 55 | interval: "daily" 56 | 57 | - package-ecosystem: "github-actions" 58 | directory: "/" 59 | schedule: 60 | interval: "daily" 61 | ``` 62 | 63 | ```yaml 64 | # .github/workflows/main.yml 65 | on: 66 | push: 67 | branches: [main] 68 | pull_request: 69 | 70 | jobs: 71 | test: 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v3 75 | - uses: actions/setup-node@v2 76 | with: 77 | node-version: '18' 78 | - run: npm install 79 | - run: npm run test 80 | - run: npm run lint 81 | - run: npm run tsc 82 | - run: npm run build 83 | ``` 84 | 85 | ### Bad ❌ 86 | 87 | ```json 88 | // package.json with inconsistent versioning 89 | { 90 | "devDependencies": { 91 | "@vitest/coverage-v8": "^3.0.5", 92 | "typescript": "~5.7.3", 93 | "vitest": "2.0.1" 94 | } 95 | } 96 | ``` 97 | 98 | ```yaml 99 | # Missing or incomplete dependabot.yml 100 | updates: 101 | - package-ecosystem: "npm" 102 | directory: "/" 103 | # Missing version and schedule 104 | ``` 105 | 106 | ```yaml 107 | # Incomplete CI workflow 108 | on: 109 | push: 110 | branches: [main] 111 | # Missing pull_request trigger 112 | 113 | jobs: 114 | test: 115 | runs-on: ubuntu-latest 116 | steps: 117 | - uses: actions/checkout@v3 118 | - run: npm install 119 | - run: npm run test 120 | # Missing linting, type checking, and build steps 121 | ``` -------------------------------------------------------------------------------- /.wispbit/rules/type-safety-dual-implementation.md: -------------------------------------------------------------------------------- 1 | --- 2 | include: *.ts, *.tsx 3 | --- 4 | 5 | # Type Safety and Dual Implementation Pattern 6 | 7 | All string utilities in this codebase must follow the dual implementation pattern: 8 | 9 | 1. Define a type-level implementation that transforms types using TypeScript's type system 10 | 2. Implement a corresponding runtime function that performs the same transformation 11 | 3. Ensure both implementations handle edge cases consistently 12 | 4. Preserve string literal types whenever possible rather than widening to `string` 13 | 14 | ## Examples 15 | 16 | ### Good ✅ 17 | ```typescript 18 | /** 19 | * Reverses a string in the type system. 20 | */ 21 | export type Reverse< 22 | T extends string, 23 | _acc extends string = '', 24 | > = T extends `${infer Head}${infer Tail}` 25 | ? Reverse 26 | : _acc extends '' 27 | ? T 28 | : `${T}${_acc}` 29 | 30 | /** 31 | * A strongly-typed version that reverses a string. 32 | * @param str the string to reverse. 33 | * @returns the reversed string in both type level and runtime. 34 | * @example reverse('hello') // 'olleh' 35 | */ 36 | export function reverse(str: T) { 37 | return str.split('').reverse().join('') as Reverse 38 | } 39 | ``` 40 | 41 | ### Bad ❌ 42 | ```typescript 43 | // Missing type-level implementation 44 | export function reverse(str: string): string { 45 | return str.split('').reverse().join('') 46 | } 47 | 48 | // Or type and runtime implementations that don't match 49 | export type Reverse = string // Incorrect type implementation 50 | 51 | export function reverse(str: T) { 52 | return str.split('').reverse().join('') 53 | } 54 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gustavo Guichard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | StringTs Banner 3 |

4 | 5 | [![NPM](https://img.shields.io/npm/v/string-ts)](https://www.npmjs.org/package/string-ts) 6 | ![Library size](https://img.shields.io/bundlephobia/minzip/string-ts) 7 | [![Code Coverage](https://img.shields.io/codecov/c/github/gustavoguichard/string-ts)](https://app.codecov.io/gh/gustavoguichard/string-ts) 8 | [![All Contributors](https://img.shields.io/github/contributors/gustavoguichard/string-ts)](#-contributors) 9 | 10 | ## Strongly-typed string functions for all! 11 | 12 | ![A demonstration of string-ts](https://github.com/gustavoguichard/string-ts/assets/566971/0aa5603f-871d-4eb7-8ace-6a73466cec4d) 13 | 14 | ## 😬 The problem 15 | 16 | When you are working with literal strings, the string manipulation functions only work at the runtime level and the types don't follow those transformations. 17 | You end up losing type information and possibly having to cast the result. 18 | 19 | ```ts 20 | const str = 'hello-world' 21 | const result = str.replace('-', ' ') // you should use: as 'hello world' 22 | // ^? string 23 | ``` 24 | 25 | ## 🤓 The solution 26 | 27 | This library aims to solve this problem by providing a set of common functions that work with literal strings at both type and runtime level. 28 | 29 | ```ts 30 | import { replace } from 'string-ts' 31 | 32 | const str = 'hello-world' 33 | const result = replace(str, '-', ' ') 34 | // ^ 'hello world' 35 | ``` 36 | 37 | ## 🔍 Why this matters 38 | 39 | TypeScript yields the best static analysis when types are highly specific. 40 | Literals are more specific than type `string`. 41 | This library preserves literals (and unions of literals) after transformations, unlike most existing utility libraries (and built-in string methods.) 42 | 43 | [I still don't get the purpose of this library 🤔](#%EF%B8%8F-interview) 44 | 45 | ### In-depth example 46 | 47 | In the below example, I want to get a strongly-typed, camel-case version of `process.env`. 48 | One flow results in a loose type, and the other results in a more precise type. 49 | This example should illustrate the highly-specific and flexible nature of `string-ts`. 50 | 51 | ```ts 52 | import { deepCamelKeys } from 'string-ts' 53 | import { camelCase, mapKeys } from 'lodash-es' 54 | import z from 'zod' 55 | 56 | const EnvSchema = z.object({ 57 | NODE_ENV: z.string(), 58 | }) 59 | 60 | function getEnvLoose() { 61 | const rawEnv = EnvSchema.parse(process.env) 62 | const env = mapKeys(rawEnv, (_v, k) => camelCase(k)) 63 | // ^? Dictionary 64 | 65 | // `Dictionary` is too loose 66 | // TypeScript is okay with this, 'abc' is expected to be of type `string` 67 | // This will have unexpected behavior at runtime 68 | console.log(env.abc) 69 | } 70 | 71 | function getEnvPrecise() { 72 | const rawEnv = EnvSchema.parse(process.env) 73 | const env = deepCamelKeys(rawEnv) 74 | // ^? { nodeEnv: string } 75 | 76 | // Error: Property 'abc' does not exist on type '{ nodeEnv: string; }' 77 | // Our type is more specific, so TypeScript catches this error. 78 | // This mistake will be caught at compile time 79 | console.log(env.abc) 80 | } 81 | 82 | function main() { 83 | getEnvLoose() 84 | getEnvPrecise() 85 | } 86 | 87 | main() 88 | ``` 89 | 90 | ## 📦 Installation 91 | 92 | ```bash 93 | npm install string-ts 94 | ``` 95 | 96 | ## 🌳 Tree shaking 97 | 98 | `string-ts` has been designed with tree shaking in mind. 99 | We have tested it with build tools like Webpack, Vite, Rollup, etc. 100 | 101 | ## 👌 Supported TypeScript versions 102 | 103 | `string-ts` currently only works on TypeScript v5+. 104 | 105 | It also only work with common ASCII characters characters. We don't plan to support international characters or emojis. 106 | 107 | --- 108 | 109 | # 📖 API 110 | 111 | - [Runtime counterparts of native type utilities](#runtime-counterparts-of-native-type-utilities) 112 | - [capitalize](#capitalize) 113 | - [uncapitalize](#uncapitalize) 114 | - [Strongly-typed alternatives to native runtime utilities](#strongly-typed-alternatives-to-native-runtime-utilities) 115 | - [charAt](#charat) 116 | - [concat](#concat) 117 | - [endsWith](#endsWith) 118 | - [includes](#includes) 119 | - [join](#join) 120 | - [length](#length) 121 | - [padEnd](#padend) 122 | - [padStart](#padstart) 123 | - [repeat](#repeat) 124 | - [replace](#replace) 125 | - [replaceAll](#replaceall) 126 | - [slice](#slice) 127 | - [split](#split) 128 | - [startsWith](#startsWith) 129 | - [toLowerCase](#tolowercase) 130 | - [toUpperCase](#touppercase) 131 | - [trim](#trim) 132 | - [trimEnd](#trimend) 133 | - [trimStart](#trimstart) 134 | - [Strongly-typed alternatives to common loosely-typed functions](#strongly-typed-alternatives-to-common-loosely-typed-functions) 135 | - [camelCase](#camelcase) 136 | - [constantCase](#constantcase) 137 | - [delimiterCase](#delimitercase) 138 | - [kebabCase](#kebabcase) 139 | - [pascalCase](#pascalcase) 140 | - [reverse](#reverse) 141 | - [snakeCase](#snakecase) 142 | - [titleCase](#titlecase) 143 | - [truncate](#truncate) 144 | - [words](#words) 145 | - [Strongly-typed shallow transformation of objects](#strongly-typed-shallow-transformation-of-objects) 146 | - [camelKeys](#camelkeys) 147 | - [constantKeys](#constantkeys) 148 | - [delimiterKeys](#delimiterkeys) 149 | - [kebabKeys](#kebabkeys) 150 | - [pascalKeys](#pascalkeys) 151 | - [replaceKeys](#replacekeys) 152 | - [snakeKeys](#snakekeys) 153 | - [Strongly-typed deep transformation of objects](#strongly-typed-deep-transformation-of-objects) 154 | - [deepCamelKeys](#deepcamelkeys) 155 | - [deepConstantKeys](#deepconstantkeys) 156 | - [deepDelimiterKeys](#deepdelimiterkeys) 157 | - [deepKebabKeys](#deepkebabkeys) 158 | - [deepPascalKeys](#deeppascalkeys) 159 | - [deepSnakeKeys](#deepsnakekeys) 160 | - [Type Utilities](#type-utilities) 161 | - [Native TS type utilities](#native-ts-type-utilities) 162 | - [General Type utilities from this library](#general-type-utilities-from-this-library) 163 | - [Casing type utilities](#casing-type-utilities) 164 | - [Other exported type utilities](#other-exported-type-utilities) 165 | - [Runtime-only utilities](#runtime-only-utilities) 166 | - [deepTransformKeys](#deeptransformkeys) 167 | 168 | --- 169 | 170 | ## Runtime counterparts of native type utilities 171 | 172 | ### capitalize 173 | 174 | Capitalizes the first letter of a string. This is a runtime counterpart of `Capitalize` from `src/types.d.ts`. 175 | 176 | ```ts 177 | import { capitalize } from 'string-ts' 178 | 179 | const str = 'hello world' 180 | const result = capitalize(str) 181 | // ^ 'Hello world' 182 | ``` 183 | 184 | ### uncapitalize 185 | 186 | Uncapitalizes the first letter of a string. This is a runtime counterpart of `Uncapitalize` from `src/types.d.ts`. 187 | 188 | ```ts 189 | import { uncapitalize } from 'string-ts' 190 | 191 | const str = 'Hello world' 192 | const result = uncapitalize(str) 193 | // ^ 'hello world' 194 | ``` 195 | 196 | ## Strongly-typed alternatives to native runtime utilities 197 | 198 | ### charAt 199 | 200 | This function is a strongly-typed counterpart of `String.prototype.charAt`. 201 | 202 | ```ts 203 | import { charAt } from 'string-ts' 204 | 205 | const str = 'hello world' 206 | const result = charAt(str, 6) 207 | // ^ 'w' 208 | ``` 209 | 210 | ### concat 211 | 212 | This function is a strongly-typed counterpart of `String.prototype.concat`. 213 | 214 | ```ts 215 | import { concat } from 'string-ts' 216 | 217 | const result = concat('a', 'bc', 'def') 218 | // ^ 'abcdef' 219 | ``` 220 | 221 | ### endsWith 222 | 223 | This function is a strongly-typed counterpart of `String.prototype.endsWith`. 224 | 225 | ```ts 226 | import { endsWith } from 'string-ts' 227 | 228 | const result = endsWith('abc', 'c') 229 | // ^ true 230 | ``` 231 | 232 | ### includes 233 | 234 | This function is a strongly-typed counterpart of `String.prototype.includes`. 235 | 236 | ```ts 237 | import { includes } from 'string-ts' 238 | 239 | const result = includes('abcde', 'bcd') 240 | // ^ true 241 | ``` 242 | 243 | ### join 244 | 245 | This function is a strongly-typed counterpart of `Array.prototype.join`. 246 | 247 | ```ts 248 | import { join } from 'string-ts' 249 | 250 | const str = ['hello', 'world'] 251 | const result = join(str, ' ') 252 | // ^ 'hello world' 253 | ``` 254 | 255 | ### length 256 | 257 | This function is a strongly-typed counterpart of `String.prototype.length`. 258 | 259 | ```ts 260 | import { length } from 'string-ts' 261 | 262 | const str = 'hello' 263 | const result = length(str) 264 | // ^ 5 265 | ``` 266 | 267 | ### padEnd 268 | 269 | This function is a strongly-typed counterpart of `String.prototype.padEnd`. 270 | 271 | ```ts 272 | import { padEnd } from 'string-ts' 273 | 274 | const str = 'hello' 275 | const result = padEnd(str, 10, '=') 276 | // ^ 'hello=====' 277 | ``` 278 | 279 | ### padStart 280 | 281 | This function is a strongly-typed counterpart of `String.prototype.padStart`. 282 | 283 | ```ts 284 | import { padStart } from 'string-ts' 285 | 286 | const str = 'hello' 287 | const result = padStart(str, 10, '=') 288 | // ^ '=====hello' 289 | ``` 290 | 291 | ### repeat 292 | 293 | This function is a strongly-typed counterpart of `String.prototype.repeat`. 294 | 295 | ```ts 296 | import { repeat } from 'string-ts' 297 | 298 | const str = 'abc' 299 | const result = repeat(str, 3) 300 | // ^ 'abcabcabc' 301 | ``` 302 | 303 | ### replace 304 | 305 | This function is a strongly-typed counterpart of `String.prototype.replace`. 306 | 307 | _Warning: this is a partial implementation, as we don't fully support Regex. Using a RegExp lookup will result in a loose typing._ 308 | 309 | ```ts 310 | import { replace } from 'string-ts' 311 | 312 | const str = 'hello-world-' 313 | const result = replace(str, '-', ' ') 314 | // ^ 'hello world-' 315 | const looselyTypedResult = replace(str, /-/, ' ') 316 | // ^ string 317 | ``` 318 | 319 | ### replaceAll 320 | 321 | This function is a strongly-typed counterpart of `String.prototype.replaceAll`. 322 | It also has a polyfill for runtimes older than ES2021. 323 | 324 | _Warning: this is a partial implementation, as we don't fully support Regex. Using a RegExp lookup will result in a loose typing._ 325 | 326 | ```ts 327 | import { replaceAll } from 'string-ts' 328 | 329 | const str = 'hello-world-' 330 | const result = replaceAll(str, '-', ' ') 331 | // ^ 'hello world ' 332 | const looselyTypedResult = replaceAll(str, /-/g, ' ') 333 | // ^ string 334 | ``` 335 | 336 | ### slice 337 | 338 | This function is a strongly-typed counterpart of `String.prototype.slice`. 339 | 340 | ```ts 341 | import { slice } from 'string-ts' 342 | 343 | const str = 'hello-world' 344 | const result = slice(str, 6) 345 | // ^ 'world' 346 | const result2 = slice(str, 1, 5) 347 | // ^ 'ello' 348 | const result3 = slice(str, -5) 349 | // ^ 'world' 350 | ``` 351 | 352 | ### split 353 | 354 | This function is a strongly-typed counterpart of `String.prototype.split`. 355 | 356 | ```ts 357 | import { split } from 'string-ts' 358 | 359 | const str = 'hello-world' 360 | const result = split(str, '-') 361 | // ^ ['hello', 'world'] 362 | ``` 363 | 364 | ### startsWith 365 | 366 | This function is a strongly-typed counterpart of `String.prototype.startsWith`. 367 | 368 | ```ts 369 | import { startsWith } from 'string-ts' 370 | 371 | const result = startsWith('abc', 'a') 372 | // ^ true 373 | ``` 374 | 375 | ### toLowerCase 376 | 377 | This function is a strongly-typed counterpart of `String.prototype.toLowerCase`. 378 | 379 | ```ts 380 | import { toLowerCase } from 'string-ts' 381 | 382 | const str = 'HELLO WORLD' 383 | const result = toLowerCase(str) 384 | // ^ 'hello world' 385 | ``` 386 | 387 | ### toUpperCase 388 | 389 | This function is a strongly-typed counterpart of `String.prototype.toUpperCase`. 390 | 391 | ```ts 392 | import { toUpperCase } from 'string-ts' 393 | 394 | const str = 'hello world' 395 | const result = toUpperCase(str) 396 | // ^ 'HELLO WORLD' 397 | ``` 398 | 399 | ### trim 400 | 401 | This function is a strongly-typed counterpart of `String.prototype.trim`. 402 | 403 | ```ts 404 | import { trim } from 'string-ts' 405 | 406 | const str = ' hello world ' 407 | const result = trim(str) 408 | // ^ 'hello world' 409 | ``` 410 | 411 | ### trimEnd 412 | 413 | This function is a strongly-typed counterpart of `String.prototype.trimEnd`. 414 | 415 | ```ts 416 | import { trimEnd } from 'string-ts' 417 | 418 | const str = ' hello world ' 419 | const result = trimEnd(str) 420 | // ^ ' hello world' 421 | ``` 422 | 423 | ### trimStart 424 | 425 | This function is a strongly-typed counterpart of `String.prototype.trimStart`. 426 | 427 | ```ts 428 | import { trimStart } from 'string-ts' 429 | 430 | const str = ' hello world ' 431 | const result = trimStart(str) 432 | // ^ 'hello world ' 433 | ``` 434 | 435 | ## Strongly-typed alternatives to common loosely-typed functions 436 | 437 | ### lowerCase 438 | 439 | This function converts a string to `lower case` at both runtime and type levels. 440 | _NOTE: this function will split by words and join them with `" "`, unlike `toLowerCase`._ 441 | 442 | ```ts 443 | import { lowerCase } from 'string-ts' 444 | 445 | const str = 'HELLO-WORLD' 446 | const result = lowerCase(str) 447 | // ^ 'hello world' 448 | ``` 449 | 450 | ### camelCase 451 | 452 | This function converts a string to `camelCase` at both runtime and type levels. 453 | 454 | ```ts 455 | import { camelCase } from 'string-ts' 456 | 457 | const str = 'hello-world' 458 | const result = camelCase(str) 459 | // ^ 'helloWorld' 460 | ``` 461 | 462 | ### constantCase 463 | 464 | This function converts a string to `CONSTANT_CASE` at both runtime and type levels. 465 | 466 | ```ts 467 | import { constantCase } from 'string-ts' 468 | 469 | const str = 'helloWorld' 470 | const result = constantCase(str) 471 | // ^ 'HELLO_WORLD' 472 | ``` 473 | 474 | ### delimiterCase 475 | 476 | This function converts a string to a new case with a custom delimiter at both runtime and type levels. 477 | 478 | ```ts 479 | import { delimiterCase } from 'string-ts' 480 | 481 | const str = 'helloWorld' 482 | const result = delimiterCase(str, '.') 483 | // ^ 'hello.World' 484 | ``` 485 | 486 | ### kebabCase 487 | 488 | This function converts a string to `kebab-case` at both runtime and type levels. 489 | 490 | ```ts 491 | import { kebabCase } from 'string-ts' 492 | 493 | const str = 'helloWorld' 494 | const result = kebabCase(str) 495 | // ^ 'hello-world' 496 | ``` 497 | 498 | ### pascalCase 499 | 500 | This function converts a string to `PascalCase` at both runtime and type levels. 501 | 502 | ```ts 503 | import { pascalCase } from 'string-ts' 504 | 505 | const str = 'hello-world' 506 | const result = pascalCase(str) 507 | // ^ 'HelloWorld' 508 | ``` 509 | 510 | ### snakeCase 511 | 512 | This function converts a string to `snake_case` at both runtime and type levels. 513 | 514 | ```ts 515 | import { snakeCase } from 'string-ts' 516 | 517 | const str = 'helloWorld' 518 | const result = snakeCase(str) 519 | // ^ 'hello_world' 520 | ``` 521 | 522 | ### titleCase 523 | 524 | This function converts a string to `Title Case` at both runtime and type levels. 525 | 526 | ```ts 527 | import { titleCase } from 'string-ts' 528 | 529 | const str = 'helloWorld' 530 | const result = titleCase(str) 531 | // ^ 'Hello World' 532 | ``` 533 | 534 | ### upperCase 535 | 536 | This function converts a string to `UPPER CASE` at both runtime and type levels. 537 | _NOTE: this function will split by words and join them with `" "`, unlike `toUpperCase`._ 538 | 539 | ```ts 540 | import { upperCase } from 'string-ts' 541 | 542 | const str = 'hello-world' 543 | const result = upperCase(str) 544 | // ^ 'HELLO WORLD' 545 | ``` 546 | 547 | ### reverse 548 | 549 | This function reverses a string. 550 | 551 | ```ts 552 | import { reverse } from 'string-ts' 553 | 554 | const str = 'Hello StringTS!' 555 | const result = reverse(str) 556 | // ^ '!TSgnirtS olleH' 557 | ``` 558 | 559 | ### truncate 560 | 561 | This function truncates string if it's longer than the given maximum string length. The last characters of the truncated string are replaced with the omission string which defaults to "...". 562 | 563 | ```ts 564 | import { truncate } from 'string-ts' 565 | 566 | const str = '-20someVery-weird String' 567 | const result = truncate(str, 8) 568 | // ^ '-20so...' 569 | ``` 570 | 571 | ### words 572 | 573 | This function identifies the words in a string and returns a tuple of words split by separators, differences in casing, numbers, and etc. 574 | 575 | ```ts 576 | import { words } from 'string-ts' 577 | 578 | const str = '-20someVery-weird String' 579 | const result = words(str) 580 | // ^ ['20', 'some', 'Very', 'weird', 'String'] 581 | ``` 582 | 583 | ## Strongly-typed shallow transformation of objects 584 | 585 | ### camelKeys 586 | 587 | This function shallowly converts the keys of an object to `camelCase` at both runtime and type levels. 588 | 589 | ```ts 590 | import { camelKeys } from 'string-ts' 591 | 592 | const data = { 593 | 'hello-world': { 594 | 'foo-bar': 'baz', 595 | }, 596 | } as const 597 | const result = camelKeys(data) 598 | // ^ { helloWorld: { 'foo-bar': 'baz' } } 599 | ``` 600 | 601 | ### constantKeys 602 | 603 | This function shallowly converts the keys of an object to `CONSTANT_CASE` at both runtime and type levels. 604 | 605 | ```ts 606 | import { constantKeys } from 'string-ts' 607 | 608 | const data = { 609 | helloWorld: { 610 | fooBar: 'baz', 611 | }, 612 | } as const 613 | const result = constantKeys(data) 614 | // ^ { 'HELLO_WORLD': { 'fooBar': 'baz' } } 615 | ``` 616 | 617 | ### delimiterKeys 618 | 619 | This function shallowly converts the keys of an object to a new case with a custom delimiter at both runtime and type levels. 620 | 621 | ```ts 622 | import { delimiterKeys } from 'string-ts' 623 | 624 | const data = { 625 | 'hello-world': { 626 | 'foo-bar': 'baz', 627 | }, 628 | } as const 629 | const result = delimiterKeys(data, '.') 630 | // ^ { 'hello.world': { 'foo-bar': 'baz' } } 631 | ``` 632 | 633 | ### kebabKeys 634 | 635 | This function shallowly converts the keys of an object to `kebab-case` at both runtime and type levels. 636 | 637 | ```ts 638 | import { kebabKeys } from 'string-ts' 639 | 640 | const data = { 641 | helloWorld: { 642 | fooBar: 'baz', 643 | }, 644 | } as const 645 | const result = kebabKeys(data) 646 | // ^ { 'hello-world': { fooBar: 'baz' } } 647 | ``` 648 | 649 | ### pascalKeys 650 | 651 | This function shallowly converts the keys of an object to `PascalCase` at both runtime and type levels. 652 | 653 | ```ts 654 | import { pascalKeys } from 'string-ts' 655 | 656 | const data = { 657 | 'hello-world': { 658 | 'foo-bar': 'baz', 659 | }, 660 | } as const 661 | const result = pascalKeys(data) 662 | // ^ { HelloWorld: { FooBar: 'baz' } } 663 | ``` 664 | 665 | ### snakeKeys 666 | 667 | This function shallowly converts the keys of an object to `snake_case` at both runtime and type levels. 668 | 669 | ```ts 670 | import { snakeKeys } from 'string-ts' 671 | 672 | const data = { 673 | helloWorld: { 674 | fooBar: 'baz', 675 | }, 676 | } as const 677 | const result = snakeKeys(data) 678 | // ^ { 'hello_world': { 'fooBar': 'baz' } } 679 | ``` 680 | 681 | ### replaceKeys 682 | 683 | This function shallowly transforms the keys of an object by applying [`replace`](#replace) to each of its keys at both runtime and type levels. 684 | 685 | ```ts 686 | import { replaceKeys } from 'string-ts' 687 | 688 | const data = { 689 | helloWorld: { 690 | fooBar: 'baz', 691 | }, 692 | } as const 693 | const result = replaceKeys(data, 'o', 'a') 694 | // ^ { 'hellaWorld': { 'fooBar': 'baz' } } 695 | ``` 696 | 697 | ## Strongly-typed deep transformation of objects 698 | 699 | ### deepCamelKeys 700 | 701 | This function recursively converts the keys of an object to `camelCase` at both runtime and type levels. 702 | 703 | ```ts 704 | import { deepCamelKeys } from 'string-ts' 705 | 706 | const data = { 707 | 'hello-world': { 708 | 'foo-bar': 'baz', 709 | }, 710 | } as const 711 | const result = deepCamelKeys(data) 712 | // ^ { helloWorld: { fooBar: 'baz' } } 713 | ``` 714 | 715 | ### deepConstantKeys 716 | 717 | This function recursively converts the keys of an object to `CONSTANT_CASE` at both runtime and type levels. 718 | 719 | ```ts 720 | import { deepConstantKeys } from 'string-ts' 721 | 722 | const data = { 723 | helloWorld: { 724 | fooBar: 'baz', 725 | }, 726 | } as const 727 | const result = deepConstantKeys(data) 728 | // ^ { 'HELLO_WORLD': { 'FOO_BAR': 'baz' } } 729 | ``` 730 | 731 | ### deepDelimiterKeys 732 | 733 | This function recursively converts the keys of an object to a new case with a custom delimiter at both runtime and type levels. 734 | 735 | ```ts 736 | import { deepDelimiterKeys } from 'string-ts' 737 | 738 | const data = { 739 | 'hello-world': { 740 | 'foo-bar': 'baz', 741 | }, 742 | } as const 743 | const result = deepDelimiterKeys(data, '.') 744 | // ^ { 'hello.world': { 'foo.bar': 'baz' } } 745 | ``` 746 | 747 | ### deepKebabKeys 748 | 749 | This function recursively converts the keys of an object to `kebab-case` at both runtime and type levels. 750 | 751 | ```ts 752 | import { deepKebabKeys } from 'string-ts' 753 | 754 | const data = { 755 | helloWorld: { 756 | fooBar: 'baz', 757 | }, 758 | } as const 759 | const result = deepKebabKeys(data) 760 | // ^ { 'hello-world': { 'foo-bar': 'baz' } } 761 | ``` 762 | 763 | ### deepPascalKeys 764 | 765 | This function recursively converts the keys of an object to `PascalCase` at both runtime and type levels. 766 | 767 | ```ts 768 | import { deepPascalKeys } from 'string-ts' 769 | 770 | const data = { 771 | 'hello-world': { 772 | 'foo-bar': 'baz', 773 | }, 774 | } as const 775 | const result = deepPascalKeys(data) 776 | // ^ { HelloWorld: { FooBar: 'baz' } } 777 | ``` 778 | 779 | ### deepSnakeKeys 780 | 781 | This function recursively converts the keys of an object to `snake_case` at both runtime and type levels. 782 | 783 | ```ts 784 | import { deepSnakeKeys } from 'string-ts' 785 | 786 | const data = { 787 | helloWorld: { 788 | fooBar: 'baz', 789 | }, 790 | } as const 791 | const result = deepSnakeKeys(data) 792 | // ^ { 'hello_world': { 'foo_bar': 'baz' } } 793 | ``` 794 | 795 | ## Type utilities 796 | 797 | All the functions presented in this API have associated type counterparts. 798 | 799 | ```ts 800 | import type * as St from 'string-ts' 801 | ``` 802 | 803 | ### Native TS type utilities 804 | 805 | ```ts 806 | Capitalize<'hello world'> // 'Hello world' 807 | Lowercase<'HELLO WORLD'> // 'hello world' 808 | Uppercase<'hello world'> // 'HELLO WORLD' 809 | ``` 810 | 811 | ### General type utilities from this library 812 | 813 | ```ts 814 | St.CharAt<'hello world', 6> // 'w' 815 | St.Concat<['a', 'bc', 'def']> // 'abcdef' 816 | St.EndsWith<'abc', 'c'> // true 817 | St.Includes<'abcde', 'bcd'> // true 818 | St.Join<['hello', 'world'], '-'> // 'hello-world' 819 | St.Length<'hello'> // 5 820 | St.PadEnd<'hello', 10, '='> // 'hello=====' 821 | St.PadStart<'hello', 10, '='> // '=====hello' 822 | St.Repeat<'abc', 3> // 'abcabcabc' 823 | St.Replace<'hello-world', 'l', '1'> // 'he1lo-world' 824 | St.ReplaceAll<'hello-world', 'l', '1'> // 'he11o-wor1d' 825 | St.Reverse<'Hello World!'> // '!dlroW olleH' 826 | St.Slice<'hello-world', -5> // 'world' 827 | St.Split<'hello-world', '-'> // ['hello', 'world'] 828 | St.Trim<' hello world '> // 'hello world' 829 | St.StartsWith<'abc', 'a'> // true 830 | St.TrimEnd<' hello world '> // ' hello world' 831 | St.TrimStart<' hello world '> // 'hello world ' 832 | St.Truncate<'hello world', 9, '[...]'> // 'hello[...] 833 | St.Words<'hello-world'> // ['hello', 'world'] 834 | ``` 835 | 836 | ### Casing type utilities 837 | 838 | #### Core 839 | 840 | ```ts 841 | St.CamelCase<'hello-world'> // 'helloWorld' 842 | St.ConstantCase<'helloWorld'> // 'HELLO_WORLD' 843 | St.DelimiterCase<'hello world', '.'> // 'hello.world' 844 | St.KebabCase<'helloWorld'> // 'hello-world' 845 | St.PascalCase<'hello-world'> // 'HelloWorld' 846 | St.SnakeCase<'helloWorld'> // 'hello_world' 847 | St.TitleCase<'helloWorld'> // 'Hello World' 848 | ``` 849 | 850 | ##### Missing types 851 | 852 | _Note that we do not include `UpperCase` and `LowerCase` types. These would be too close to the existing TS types `Uppercase` and `Lowercase`._ 853 | 854 | One could create either by doing like so: 855 | 856 | ```ts 857 | type LowerCase = Lowercase> 858 | type UpperCase = Uppercase> 859 | // or 860 | type LowerCase = ReturnType> 861 | type UpperCase = ReturnType> 862 | ``` 863 | 864 | #### Shallow object key transformation 865 | 866 | ```ts 867 | St.CamelKeys<{ 868 | 'hello-world': { 'foo-bar': 'baz' } 869 | }> // { helloWorld: { 'foo-bar': 'baz' } } 870 | St.ConstantKeys<{ 871 | helloWorld: { fooBar: 'baz' } 872 | }> // { 'HELLO_WORLD': { fooBar: 'baz' } } 873 | St.DelimiterKeys<{ 'hello-world': { 'foo-bar': 'baz' } }, '.'> 874 | // { 'hello.world': { 'foo-bar': 'baz' } } 875 | St.KebabKeys<{ 876 | helloWorld: { fooBar: 'baz' } 877 | }> // { 'hello-world': { fooBar: 'baz' } } 878 | St.PascalKeys<{ 879 | 'hello-world': { 'foo-bar': 'baz' } 880 | }> // { HelloWorld: { 'foo-bar': 'baz' } } 881 | St.SnakeKeys<{ 882 | helloWorld: { fooBar: 'baz' } 883 | }> // { 'hello_world': { fooBar: 'baz' } } 884 | ``` 885 | 886 | #### Deep object key transformation 887 | 888 | ```ts 889 | St.DeepCamelKeys<{ 890 | 'hello-world': { 'foo-bar': 'baz' } 891 | }> // { helloWorld: { fooBar: 'baz' } } 892 | St.DeepConstantKeys<{ 893 | helloWorld: { fooBar: 'baz' } 894 | }> // { 'HELLO_WORLD': { 'FOO_BAR': 'baz' } } 895 | St.DeepDelimiterKeys< 896 | { 897 | 'hello-world': { 'foo-bar': 'baz' } 898 | }, 899 | '.' 900 | > // { 'hello.world': { 'foo.bar': 'baz' } } 901 | St.DeepKebabKeys<{ 902 | helloWorld: { fooBar: 'baz' } 903 | }> // { 'hello-world': { 'foo-bar': 'baz' } } 904 | St.DeepPascalKeys<{ 905 | 'hello-world': { 'foo-bar': 'baz' } 906 | }> // { HelloWorld: { FooBar: 'baz' } } 907 | St.DeepSnakeKeys<{ 908 | helloWorld: { fooBar: 'baz' } 909 | }> // { 'hello_world': { 'foo_bar': 'baz' } } 910 | ``` 911 | 912 | ### Other exported type utilities 913 | 914 | ```ts 915 | St.IsDigit<'a'> // false 916 | St.IsDigit<'1'> // true 917 | St.IsLetter<'a'> // true 918 | St.IsLetter<'1'> // false 919 | St.IsLower<'a'> // true 920 | St.IsLower<'A'> // false 921 | St.IsUpper<'a'> // false 922 | St.IsUpper<'A'> // true 923 | St.IsSeparator<' '> // true 924 | St.IsSeparator<'-'> // true 925 | St.IsSeparator<'a'> // false 926 | St.IsSpecial<'a'> // false 927 | St.IsSpecial<'!'> // true 928 | St.IsSpecial<' '> // false 929 | ``` 930 | 931 | ## Runtime-only utilities 932 | 933 | ### deepTransformKeys 934 | 935 | This function recursively converts the keys of an object to a custom format, but only at runtime level. 936 | 937 | ```ts 938 | import { deepTransformKeys, toUpperCase } from 'string-ts' 939 | 940 | const data = { helloWorld: 'baz' } as const 941 | 942 | type MyType = { [K in keyof T as Uppercase]: T[K] } 943 | const result = deepTransformKeys(data, toUpperCase) as MyType 944 | // ^ { 'HELLOWORLD': 'baz' } 945 | ``` 946 | 947 | ## 🎙️ Interview 948 | 949 | For a deeper dive into the code, reasoning, and how this library came to be, check out this interview with the author of StringTs on the Michigan TypeScript Meetup. 950 | 951 | [![StringTs on Michigan TS](https://img.youtube.com/vi/dOXpkAmmahw/0.jpg)](https://www.youtube.com/watch?v=dOXpkAmmahw) 952 | 953 | ## 🐝 Contributors 954 | 955 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 |
Guga Guichard
Guga Guichard

💻 📆 📣 🚧 📖 🐛 🚇 💬 🔬 👀 🤔 💡
Landon Yarrington
Landon Yarrington

💻 🚧 📖 👀 🤔 💡 💬 🐛
Guillaume
Guillaume

💻 🚧 📖 🐛 🚇 💬 🤔
Matt Pocock
Matt Pocock

📖 💻 📣
Andrew Luca
Andrew Luca

📖 📣
Mjuksel
Mjuksel

💻 🤔
hverlin
hverlin

💻
972 | 973 | 974 | 975 | 976 | 977 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 978 | 979 | StringTs logo by [NUMI](https://github.com/numi-hq/open-design): 980 | 981 | [NUMI Logo](https://numi.tech/?ref=string-ts) 982 | 983 | ## 🫶 Acknowledgements 984 | 985 | This library got a lot of inspiration from libraries such as [lodash](https://github.com/lodash/lodash), [ts-reset](https://github.com/total-typescript/ts-reset), [type-fest](https://github.com/sindresorhus/type-fest), [HOTScript](https://github.com/gvergnaud/hotscript), and many others. 986 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["dist"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space", 15 | "indentWidth": 2 16 | }, 17 | "organizeImports": { 18 | "enabled": true 19 | }, 20 | "linter": { 21 | "enabled": true, 22 | "rules": { 23 | "recommended": true, 24 | "correctness": { 25 | "noUnusedImports": { 26 | "level": "warn", 27 | "fix": "safe" 28 | } 29 | }, 30 | "style": { 31 | "noUnusedTemplateLiteral": { 32 | "level": "error", 33 | "fix": "safe" 34 | }, 35 | "useTemplate": { 36 | "level": "error", 37 | "fix": "safe" 38 | } 39 | }, 40 | "suspicious": { 41 | "noExplicitAny": "off", 42 | "noShadowRestrictedNames": "off" 43 | } 44 | } 45 | }, 46 | "javascript": { 47 | "formatter": { 48 | "quoteStyle": "single", 49 | "semicolons": "asNeeded", 50 | "trailingCommas": "es5" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /docs/string-ts-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavoguichard/string-ts/1b005999dc29048f591c719420702fa70620b897/docs/string-ts-banner.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string-ts", 3 | "version": "2.3.0-experimental.4", 4 | "description": "Strongly-typed string functions.", 5 | "author": "Gustavo Guichard <@gugaguichard>", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsup ./src/index.ts --format esm,cjs --dts --treeshake && tsx scripts/generate-entrypoints.mts", 9 | "dev": "tsup ./src/index.ts --format esm,cjs --watch --dts", 10 | "lint": "node_modules/.bin/biome check --write --error-on-warnings", 11 | "tsc": "tsc --noEmit", 12 | "tsc:dist": "tsc --project tsconfig.dist.json", 13 | "test": "vitest run" 14 | }, 15 | "exports": { 16 | ".": { 17 | "import": "./dist/index.mjs", 18 | "require": "./dist/index.js", 19 | "default": "./dist/index.mjs", 20 | "types": "./dist/index.d.ts" 21 | }, 22 | "./native": { 23 | "default": "./dist/native.js", 24 | "types": "./dist/native.d.ts" 25 | } 26 | }, 27 | "main": "./dist/index.js", 28 | "module": "./dist/index.mjs", 29 | "types": "./dist/index.d.ts", 30 | "typesVersions": { 31 | "*": { 32 | "native": ["dist/native.d.ts"] 33 | } 34 | }, 35 | "devDependencies": { 36 | "@biomejs/biome": "^1.9.4", 37 | "@types/node": "^22.15.3", 38 | "@vitest/coverage-v8": "^3.0.5", 39 | "tsup": "latest", 40 | "tsx": "^4.19.4", 41 | "typescript": "^5.7.3", 42 | "vitest": "latest" 43 | }, 44 | "files": ["README.md", "./dist/*"], 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/gustavoguichard/string-ts.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/gustavoguichard/string-ts/issues" 51 | }, 52 | "sideEffects": false, 53 | "pnpm": { 54 | "onlyBuiltDependencies": ["@biomejs/biome", "esbuild"] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/generate-entrypoints.mts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises' 2 | import * as path from 'node:path' 3 | 4 | const entry = 'native' 5 | const src = path.resolve('src', entry, 'index.d.ts') 6 | const dist = path.resolve('dist', `${entry}.d.ts`) 7 | 8 | let code = await fs.readFile(src, 'utf8') 9 | code = code.replace(/(\.\.\/[^'"]+)\.ts/g, '$1') // strip ".ts" 10 | 11 | await fs.writeFile(dist, code) 12 | await fs.writeFile(path.resolve('dist', `${entry}.js`), '') 13 | await fs.writeFile(path.resolve('dist', `${entry}.mjs`), '') 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Native 2 | export type { CharAt } from './native/char-at.js' 3 | export { charAt } from './native/char-at.js' 4 | export type { Concat } from './native/concat.js' 5 | export { concat } from './native/concat.js' 6 | export type { EndsWith } from './native/ends-with.js' 7 | export { endsWith } from './native/ends-with.js' 8 | export type { Includes } from './native/includes.js' 9 | export { includes } from './native/includes.js' 10 | export type { Join } from './native/join.js' 11 | export { join } from './native/join.js' 12 | export type { Length } from './native/length.js' 13 | export { length } from './native/length.js' 14 | export type { PadEnd } from './native/pad-end.js' 15 | export { padEnd } from './native/pad-end.js' 16 | export type { PadStart } from './native/pad-start.js' 17 | export { padStart } from './native/pad-start.js' 18 | export type { Repeat } from './native/repeat.js' 19 | export { repeat } from './native/repeat.js' 20 | export type { Replace } from './native/replace.js' 21 | export { replace } from './native/replace.js' 22 | export type { ReplaceAll } from './native/replace-all.js' 23 | export { replaceAll } from './native/replace-all.js' 24 | export type { Slice } from './native/slice.js' 25 | export { slice } from './native/slice.js' 26 | export type { Split } from './native/split.js' 27 | export { split } from './native/split.js' 28 | export type { StartsWith } from './native/starts-with.js' 29 | export { startsWith } from './native/starts-with.js' 30 | export type { TrimStart } from './native/trim-start.js' 31 | export { trimStart } from './native/trim-start.js' 32 | export type { TrimEnd } from './native/trim-end.js' 33 | export { trimEnd } from './native/trim-end.js' 34 | export type { Trim } from './native/trim.js' 35 | export { trim } from './native/trim.js' 36 | 37 | export { toLowerCase } from './native/to-lower-case.js' 38 | export { toUpperCase } from './native/to-upper-case.js' 39 | 40 | // Utils 41 | export type { Reverse } from './utils/reverse' 42 | export { reverse } from './utils/reverse' 43 | export type { Truncate } from './utils/truncate.js' 44 | export { truncate } from './utils/truncate.js' 45 | export type { Words } from './utils/words.js' 46 | export { words } from './utils/words.js' 47 | 48 | // Characters 49 | export type { IsLetter, IsLower, IsUpper } from './utils/characters/letters.js' 50 | export type { IsDigit } from './utils/characters/numbers.js' 51 | export type { IsSpecial } from './utils/characters/special.js' 52 | export type { IsSeparator } from './utils/characters/separators.js' 53 | 54 | // Word casing 55 | export type { CamelCase } from './utils/word-case/camel-case.js' 56 | export { camelCase, toCamelCase } from './utils/word-case/camel-case.js' 57 | export type { ConstantCase } from './utils/word-case/constant-case.js' 58 | export { 59 | constantCase, 60 | toConstantCase, 61 | } from './utils/word-case/constant-case.js' 62 | export type { DelimiterCase } from './utils/word-case/delimiter-case.js' 63 | export { 64 | delimiterCase, 65 | toDelimiterCase, 66 | } from './utils/word-case/delimiter-case.js' 67 | export type { KebabCase } from './utils/word-case/kebab-case.js' 68 | export { kebabCase, toKebabCase } from './utils/word-case/kebab-case.js' 69 | export type { PascalCase } from './utils/word-case/pascal-case.js' 70 | export { pascalCase, toPascalCase } from './utils/word-case/pascal-case.js' 71 | export type { SnakeCase } from './utils/word-case/snake-case.js' 72 | export { snakeCase, toSnakeCase } from './utils/word-case/snake-case.js' 73 | export type { TitleCase } from './utils/word-case/title-case.js' 74 | export { titleCase, toTitleCase } from './utils/word-case/title-case.js' 75 | 76 | export { capitalize } from './utils/word-case/capitalize.js' 77 | export { lowerCase } from './utils/word-case/lower-case.js' 78 | export { uncapitalize } from './utils/word-case/uncapitalize.js' 79 | export { upperCase } from './utils/word-case/upper-case.js' 80 | 81 | // Object keys word casing 82 | export type { CamelKeys } from './utils/object-keys/camel-keys.js' 83 | export { camelKeys } from './utils/object-keys/camel-keys.js' 84 | export type { ConstantKeys } from './utils/object-keys/constant-keys.js' 85 | export { constantKeys } from './utils/object-keys/constant-keys.js' 86 | export type { DelimiterKeys } from './utils/object-keys/delimiter-keys.js' 87 | export { delimiterKeys } from './utils/object-keys/delimiter-keys.js' 88 | export type { KebabKeys } from './utils/object-keys/kebab-keys.js' 89 | export { kebabKeys } from './utils/object-keys/kebab-keys.js' 90 | export type { PascalKeys } from './utils/object-keys/pascal-keys.js' 91 | export { pascalKeys } from './utils/object-keys/pascal-keys.js' 92 | export type { SnakeKeys } from './utils/object-keys/snake-keys.js' 93 | export { snakeKeys } from './utils/object-keys/snake-keys.js' 94 | // Object keys transformation 95 | export type { ReplaceKeys } from './utils/object-keys/replace-keys.js' 96 | export { replaceKeys } from './utils/object-keys/replace-keys.js' 97 | 98 | // Object keys word casing (deep) 99 | export type { DeepCamelKeys } from './utils/object-keys/deep-camel-keys.js' 100 | export { deepCamelKeys } from './utils/object-keys/deep-camel-keys.js' 101 | export type { DeepConstantKeys } from './utils/object-keys/deep-constant-keys.js' 102 | export { deepConstantKeys } from './utils/object-keys/deep-constant-keys.js' 103 | export type { DeepDelimiterKeys } from './utils/object-keys/deep-delimiter-keys.js' 104 | export { deepDelimiterKeys } from './utils/object-keys/deep-delimiter-keys.js' 105 | export type { DeepKebabKeys } from './utils/object-keys/deep-kebab-keys.js' 106 | export { deepKebabKeys } from './utils/object-keys/deep-kebab-keys.js' 107 | export type { DeepPascalKeys } from './utils/object-keys/deep-pascal-keys.js' 108 | export { deepPascalKeys } from './utils/object-keys/deep-pascal-keys.js' 109 | export type { DeepSnakeKeys } from './utils/object-keys/deep-snake-keys.js' 110 | export { deepSnakeKeys } from './utils/object-keys/deep-snake-keys.js' 111 | 112 | export { deepTransformKeys } from './utils/object-keys/deep-transform-keys.js' 113 | -------------------------------------------------------------------------------- /src/internal/fixtures.ts: -------------------------------------------------------------------------------- 1 | export const SEPARATORS_TEXT = 2 | '[one] two-three/four.five(six){seven}|eight_nine\\ten' as const 3 | 4 | export const WEIRD_TEXT = 5 | ' someWeird-cased$*String1986Foo [Bar] W_FOR_WUMBO...' as const 6 | 7 | export type WeirdTextUnion = 8 | | typeof WEIRD_TEXT 9 | | "where's the leak ma'am" 10 | | 'dont.distribute unions' 11 | -------------------------------------------------------------------------------- /src/internal/internals.test.ts: -------------------------------------------------------------------------------- 1 | import type { DropSuffix, PascalCaseAll, Reject, TupleOf } from './internals.js' 2 | import { pascalCaseAll, typeOf } from './internals.js' 3 | 4 | namespace Internals { 5 | type testPascalCaseAll1 = Expect< 6 | Equal, ['One', 'Two', 'Three']> 7 | > 8 | type testPascalCaseAll2 = Expect, string[]>> 9 | 10 | type testReject1 = Expect< 11 | Equal, ['one', 'two', 'three']> 12 | > 13 | 14 | type testDropSuffix1 = Expect< 15 | Equal, 'hello'> 16 | > 17 | type testDropSuffix2 = Expect, string>> 18 | type testDropSuffix3 = Expect, string>> 19 | 20 | type testTupleOf1 = Expect, [' ', ' ', ' ']>> 21 | } 22 | 23 | describe('typeOf', () => { 24 | test('null', () => { 25 | expect(typeOf(null)).toEqual('null') 26 | }) 27 | test('object', () => { 28 | expect(typeOf({})).toEqual('object') 29 | }) 30 | test('object', () => { 31 | expect(typeOf(['a', 'b', 'c'])).toEqual('array') 32 | }) 33 | test('string', () => { 34 | expect(typeOf('hello')).toEqual('string') 35 | }) 36 | }) 37 | 38 | describe('pascalCaseAll', () => { 39 | test('simple', () => { 40 | const result = pascalCaseAll(['one', 'two', 'three']) 41 | const expected = ['One', 'Two', 'Three'] 42 | expect(result).toEqual(expected) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/internal/internals.ts: -------------------------------------------------------------------------------- 1 | import { toLowerCase } from '../native/to-lower-case.js' 2 | import { capitalize } from '../utils/word-case/capitalize.js' 3 | 4 | /** 5 | * This is an enhanced version of the typeof operator to check the type of more complex values. 6 | * In this case we just mind about arrays and objects. We can add more on demand. 7 | * @param t the value to be checked 8 | * @returns the type of the value 9 | */ 10 | function typeOf(t: unknown) { 11 | return Object.prototype.toString 12 | .call(t) 13 | .replace(/^\[object (.+)\]$/, '$1') 14 | .toLowerCase() as 'array' | 'object' | (string & {}) 15 | } 16 | 17 | // MAP TYPES 18 | 19 | /** 20 | * PascalCases all the words in a tuple of strings 21 | */ 22 | type PascalCaseAll = T extends [ 23 | infer head extends string, 24 | ...infer rest extends string[], 25 | ] 26 | ? [Capitalize>, ...PascalCaseAll] 27 | : T 28 | 29 | function pascalCaseAll(words: T) { 30 | return words.map((v) => capitalize(toLowerCase(v))) as PascalCaseAll 31 | } 32 | 33 | /** 34 | * Removes all the elements matching the given condition from a tuple. 35 | */ 36 | type Reject = tuple extends [ 37 | infer first, 38 | ...infer rest, 39 | ] 40 | ? Reject 41 | : output 42 | 43 | /** 44 | * Removes the given suffix from a sentence. 45 | */ 46 | type DropSuffix = string extends 47 | | sentence 48 | | suffix 49 | ? string 50 | : sentence extends `${infer rest}${suffix}` 51 | ? rest 52 | : sentence 53 | 54 | /** 55 | * Returns a tuple of the given length with the given type. 56 | */ 57 | type TupleOf< 58 | L extends number, 59 | T = unknown, 60 | result extends any[] = [], 61 | > = result['length'] extends L ? result : TupleOf 62 | 63 | export type { Reject, DropSuffix, PascalCaseAll, TupleOf } 64 | export { pascalCaseAll, typeOf } 65 | -------------------------------------------------------------------------------- /src/internal/literals.test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | Any, 4 | IsBooleanLiteral, 5 | IsNumberLiteral, 6 | IsStringLiteral, 7 | IsStringLiteralArray, 8 | } from './literals.js' 9 | 10 | namespace LiteralsTests { 11 | // IsNumberLiteral 12 | type testINL1 = Expect>> 13 | type testINL2 = Expect>> 14 | 15 | // IsBooleanLiteral 16 | type testIBL1 = Expect, true>> 17 | type testIBL2 = Expect, true>> 18 | type testIBL3 = Expect, false>> 19 | 20 | // Any 21 | type testAny1 = Expect, true>> 22 | type testAny2 = Expect, true>> 23 | type testAny3 = Expect, false>> 24 | type testAny4 = Expect, false>> 25 | type testAny5 = Expect, false>> 26 | type testAny6 = Expect, false>> 27 | 28 | // All 29 | type testAll1 = Expect, true>> 30 | type testAll2 = Expect, false>> 31 | type testAll3 = Expect, false>> 32 | type testAll4 = Expect, false>> 33 | type testAll5 = Expect, false>> 34 | type testAll6 = Expect, true>> 35 | type testAll7 = Expect, false>> 36 | 37 | // IsStringLiteral 38 | type testISL1 = Expect>> 39 | type testISL2 = Expect>>> 40 | type testISL3 = Expect< 41 | Equal>> 42 | > 43 | type testISL4 = Expect>> 44 | type testISL5 = Expect>> 45 | type testISL6 = Expect>> 46 | type testISL7 = Expect>>> 47 | type testISL8 = Expect< 48 | Equal>>> 49 | > 50 | type testISL9 = Expect>> 51 | type testISL10 = Expect>>> 52 | type testISL11 = Expect>>> 53 | type testISL12 = Expect>> 54 | type testISL13 = Expect< 55 | Equal>> 56 | > 57 | type testISL14 = Expect>> 58 | type testISL15 = Expect>> 59 | type testISL16 = Expect>> 60 | type testISL17 = Expect>> 61 | 62 | // IsStringLiteralArray 63 | type testISLA1 = Expect, true>> 64 | type testISLA2 = Expect, false>> 65 | type testISLA3 = Expect< 66 | Equal, false> 67 | > 68 | } 69 | 70 | test('dummy test', () => expect(true).toBe(true)) 71 | -------------------------------------------------------------------------------- /src/internal/literals.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if input number type is a literal 3 | */ 4 | type IsNumberLiteral = [T] extends [number] 5 | ? [number] extends [T] 6 | ? false 7 | : true 8 | : false 9 | 10 | type IsBooleanLiteral = [T] extends [boolean] 11 | ? [boolean] extends [T] 12 | ? false 13 | : true 14 | : false 15 | 16 | /** 17 | * Returns true if any elements in boolean array are the literal true (not false or boolean) 18 | */ 19 | type Any = Arr extends [ 20 | infer Head extends boolean, 21 | ...infer Rest extends boolean[], 22 | ] 23 | ? IsBooleanLiteral extends true 24 | ? Head extends true 25 | ? true 26 | : Any 27 | : Any 28 | : false 29 | 30 | /** 31 | * Returns true if every element in boolean array is the literal true (not false or boolean) 32 | */ 33 | type All = IsBooleanLiteral extends true 34 | ? Arr extends [infer Head extends boolean, ...infer Rest extends boolean[]] 35 | ? Head extends true 36 | ? Any 37 | : false // Found `false` in array 38 | : true // Empty array (or all elements have already passed test) 39 | : false // Array/Tuple contains `boolean` type 40 | 41 | /** 42 | * Returns true if string input type is a literal 43 | */ 44 | type IsStringLiteral = [T] extends [string] 45 | ? [string] extends [T] 46 | ? false 47 | : Uppercase extends Uppercase> 48 | ? Lowercase extends Lowercase> 49 | ? true 50 | : false 51 | : false 52 | : false 53 | 54 | type IsStringLiteralArray = 55 | IsStringLiteral extends true ? true : false 56 | 57 | export type { 58 | IsNumberLiteral, 59 | IsBooleanLiteral, 60 | Any, 61 | All, 62 | IsStringLiteral, 63 | IsStringLiteralArray, 64 | } 65 | -------------------------------------------------------------------------------- /src/internal/math.test.ts: -------------------------------------------------------------------------------- 1 | import type { Math } from '../internal/math.js' 2 | 3 | namespace MathTest { 4 | // NOTE: `Subtract` only supports non-negative integers 5 | type testSubtract1 = Expect, 1>> 6 | type testSubtract2 = Expect, 0>> 7 | type testSubtract3 = Expect, number>> 8 | type testSubtract4 = Expect, number>> 9 | 10 | type testIsNegative1 = Expect, false>> 11 | type testIsNegative2 = Expect, false>> 12 | type testIsNegative3 = Expect, true>> 13 | 14 | type testAbs1 = Expect, 1>> 15 | type testAbs2 = Expect, 1>> 16 | type testAbs3 = Expect, 0>> 17 | type testAbs4 = Expect, 0>> 18 | type testAbs5 = Expect, number>> 19 | 20 | type testGetPositiveIndex1 = Expect< 21 | Equal, 2> 22 | > 23 | type testGetPositiveIndex2 = Expect< 24 | Equal, number> 25 | > 26 | } 27 | 28 | test('dummy test', () => expect(true).toBe(true)) 29 | -------------------------------------------------------------------------------- /src/internal/math.ts: -------------------------------------------------------------------------------- 1 | import type { Length } from '../native/length.js' 2 | import type { TupleOf } from './internals.js' 3 | 4 | namespace Math { 5 | export type Subtract = number extends 6 | | A 7 | | B 8 | ? number 9 | : TupleOf extends [...infer U, ...TupleOf] 10 | ? U['length'] 11 | : 0 12 | 13 | export type IsNegative = number extends T 14 | ? boolean 15 | : `${T}` extends `-${number}` 16 | ? true 17 | : false 18 | 19 | export type Abs = `${T}` extends `-${infer U extends 20 | number}` 21 | ? U 22 | : T 23 | 24 | export type GetPositiveIndex< 25 | T extends string, 26 | I extends number, 27 | > = IsNegative extends false ? I : Subtract, Abs> 28 | } 29 | 30 | export type { Math } 31 | -------------------------------------------------------------------------------- /src/internal/types.d.ts: -------------------------------------------------------------------------------- 1 | // TEST UTILITIES 2 | type Expect = T 3 | type Equal = (() => T extends A ? 1 : 2) extends () => T extends B 4 | ? 1 5 | : 2 6 | ? true 7 | : [A, 'should equal', B] 8 | -------------------------------------------------------------------------------- /src/native/char-at.test.ts: -------------------------------------------------------------------------------- 1 | import { type CharAt, charAt } from './char-at.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, 'n'>> 5 | type test2 = Expect, string>> 6 | type test3 = Expect, 5>, string>> 7 | type test4 = Expect, string>> 8 | 9 | // TODO: index greater than Length 10 | // type test4 = Expect, ''>> 11 | } 12 | 13 | describe('charAt', () => { 14 | test('should get the character of a string at the given index in both type and runtime level', () => { 15 | const data = 'some nice string' 16 | const result = charAt(data, 5) 17 | expect(result).toEqual('n') 18 | type test = Expect> 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/native/char-at.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsNumberLiteral, 4 | IsStringLiteral, 5 | } from '../internal/literals.js' 6 | import type { Split } from './split.js' 7 | 8 | /** 9 | * Gets the character at the given index. 10 | * T: The string to get the character from. 11 | * index: The index of the character. 12 | */ 13 | export type CharAt = All< 14 | [IsStringLiteral, IsNumberLiteral] 15 | > extends true 16 | ? Split[index] 17 | : string 18 | 19 | /** 20 | * A strongly-typed version of `String.prototype.charAt`. 21 | * @param str the string to get the character from. 22 | * @param index the index of the character. 23 | * @returns the character in both type level and runtime. 24 | * @example charAt('hello world', 6) // 'w' 25 | */ 26 | export function charAt( 27 | str: T, 28 | index: I 29 | ): CharAt { 30 | return str.charAt(index) as CharAt 31 | } 32 | -------------------------------------------------------------------------------- /src/native/concat.test.ts: -------------------------------------------------------------------------------- 1 | import { type Concat, concat } from './concat.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, 'abcdef'>> 5 | type test2 = Expect< 6 | Equal, 'abcdef' | '123456'> 7 | > 8 | type test3 = Expect, string>> 9 | } 10 | 11 | describe('concat', () => { 12 | test('concatenates', () => { 13 | const result = concat('one', 'two', 'three') 14 | const expected = 'onetwothree' 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/native/concat.ts: -------------------------------------------------------------------------------- 1 | import { join } from './join.js' 2 | import type { Join } from './join.js' 3 | 4 | /** 5 | * Concatenates a tuple of strings. 6 | * T: The tuple of strings to concatenate. 7 | */ 8 | export type Concat = Join 9 | 10 | /** 11 | * A strongly-typed version of `String.prototype.concat`. 12 | * @param strings the tuple of strings to concatenate. 13 | * @returns the concatenated string in both type level and runtime. 14 | * @example concat('a', 'bc', 'def') // 'abcdef' 15 | */ 16 | export function concat(...strings: T): Concat { 17 | return join(strings) 18 | } 19 | -------------------------------------------------------------------------------- /src/native/ends-with.test.ts: -------------------------------------------------------------------------------- 1 | import { type EndsWith, endsWith } from './ends-with.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, true>> 5 | type test2 = Expect, boolean>> 6 | type test3 = Expect, 'c'>, boolean>> 7 | type test4 = Expect, boolean>> 8 | type test6 = Expect, true>> 9 | type test7 = Expect, false>> 10 | type test8 = Expect, true>> 11 | type test9 = Expect, false>> 12 | 13 | // Template strings 14 | type testTS1 = Expect, true>> 15 | type testTS2 = Expect, false>> 16 | type testTS3 = Expect, true>> 17 | type testTS4 = Expect, false>> 18 | type testTS5 = Expect, true>> 19 | type testTS6 = Expect, boolean>> 20 | } 21 | 22 | describe('endsWith', () => { 23 | const text = 'abc' 24 | 25 | describe('without offset', () => { 26 | test('should return true when text ends with search', () => { 27 | const result = endsWith(text, 'c') 28 | expect(result).toEqual(true) 29 | type test = Expect> 30 | }) 31 | test('should return false when text does not end with search', () => { 32 | const result = endsWith(text, 'b') 33 | expect(result).toEqual(false) 34 | type test = Expect> 35 | }) 36 | }) 37 | 38 | describe('with offset', () => { 39 | test('should return true when offset text ends with search', () => { 40 | const result = endsWith(text, 'b', 2) 41 | expect(result).toEqual(true) 42 | type test = Expect> 43 | }) 44 | test('should return true when offset text ends with search (multi-char)', () => { 45 | const result = endsWith(text, 'bc', 3) 46 | expect(result).toEqual(true) 47 | type test = Expect> 48 | }) 49 | test('should return false when offset string does not end with search', () => { 50 | const result = endsWith(text, 'c', 1) 51 | expect(result).toEqual(false) 52 | type test = Expect> 53 | }) 54 | }) 55 | 56 | describe('with bad offset', () => { 57 | test('should return false when the offset is negative', () => { 58 | const result = endsWith(text, 'a', -1) 59 | expect(result).toEqual(false) 60 | type test = Expect> 61 | }) 62 | test('should return true when the end matches and offset is greater than text length', () => { 63 | const result = endsWith(text, 'c', 10) 64 | expect(result).toEqual(true) 65 | type test = Expect> 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/native/ends-with.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsNumberLiteral, 4 | IsStringLiteral, 5 | } from '../internal/literals.js' 6 | import type { Math } from '../internal/math.js' 7 | import type { Reverse } from '../utils/reverse.js' 8 | import type { Length } from './length.js' 9 | import type { Slice } from './slice.js' 10 | import type { StartsWith } from './starts-with.js' 11 | 12 | /** 13 | * Checks if a string ends with another string. 14 | * T: The string to check. 15 | * S: The string to check against. 16 | * P: The position the search should end. 17 | */ 18 | export type EndsWith< 19 | T extends string, 20 | S extends string, 21 | P extends number | undefined = undefined, 22 | > = P extends number ? _EndsWith : _EndsWithNoPosition 23 | 24 | type _EndsWith = All< 25 | [IsStringLiteral, IsNumberLiteral

] 26 | > extends true 27 | ? Math.IsNegative

extends false 28 | ? P extends Length 29 | ? IsStringLiteral extends true 30 | ? S extends Slice, Length>, Length> 31 | ? true 32 | : false 33 | : _EndsWithNoPosition, S> // Eg: EndsWith<`abc${string}xyz`, 'c', 3> 34 | : _EndsWithNoPosition, S> // P !== T.length, slice 35 | : false // P is negative, false 36 | : boolean 37 | 38 | /** Overload of EndsWith without P */ 39 | type _EndsWithNoPosition = StartsWith< 40 | Reverse, 41 | Reverse 42 | > 43 | 44 | /** 45 | * A strongly-typed version of `String.prototype.endsWith`. 46 | * @param text the string to search. 47 | * @param search the string to search with. 48 | * @param position the index the search should end at. 49 | * @returns boolean, whether or not the text string ends with the search string. 50 | * @example endsWith('abc', 'c') // true 51 | */ 52 | export function endsWith< 53 | T extends string, 54 | S extends string, 55 | P extends number = Length, 56 | >(text: T, search: S, position = text.length as P) { 57 | return text.endsWith(search, position) as EndsWith 58 | } 59 | -------------------------------------------------------------------------------- /src/native/includes.test.ts: -------------------------------------------------------------------------------- 1 | import { type Includes, includes } from './includes.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, true>> 5 | type test2 = Expect, boolean>> 6 | type test3 = Expect, boolean>> 7 | } 8 | 9 | describe('includes', () => { 10 | const text = 'abcde' 11 | 12 | describe('without offset', () => { 13 | test('should return true when text contains search', () => { 14 | const result = includes(text, 'bcd') 15 | expect(result).toEqual(true) 16 | type test = Expect> 17 | }) 18 | test('should return false when text does not end with search', () => { 19 | const result = includes(text, 'hello') 20 | expect(result).toEqual(false) 21 | type test = Expect> 22 | }) 23 | }) 24 | 25 | describe('with offset', () => { 26 | test('should return true when offset text does contain search', () => { 27 | const result = includes(text, 'c', 1) 28 | expect(result).toEqual(true) 29 | type test = Expect> 30 | }) 31 | test('should return true when offset text does contain search (multi-char)', () => { 32 | const result = includes(text, 'bcd', 1) 33 | expect(result).toEqual(true) 34 | type test = Expect> 35 | }) 36 | test('should return false when offset string does not contain search', () => { 37 | const result = includes(text, 'abc', 3) 38 | expect(result).toEqual(false) 39 | type test = Expect> 40 | }) 41 | }) 42 | 43 | describe('with bad offset', () => { 44 | test('should ignore offset when the offset is negative', () => { 45 | const result = includes(text, 'a', -100) 46 | expect(result).toEqual(true) 47 | type test = Expect> 48 | }) 49 | test('should return false when text contains search but offset is greater than text length', () => { 50 | const result = includes(text, 'c', 10) 51 | expect(result).toEqual(false) 52 | type test = Expect> 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/native/includes.ts: -------------------------------------------------------------------------------- 1 | import type { Math } from '../internal/math.js' 2 | import type { Slice } from './slice.js' 3 | 4 | /** 5 | * Checks if a string includes another string. 6 | * T: The string to check. 7 | * S: The string to check against. 8 | * P: The position to start the search. 9 | */ 10 | export type Includes< 11 | T extends string, 12 | S extends string, 13 | P extends number = 0, 14 | > = string extends T | S 15 | ? boolean 16 | : Math.IsNegative

extends false 17 | ? P extends 0 18 | ? T extends `${string}${S}${string}` 19 | ? true 20 | : false 21 | : Includes, S, 0> // P is >0, slice 22 | : Includes // P is negative, ignore it 23 | 24 | /** 25 | * A strongly-typed version of `String.prototype.includes`. 26 | * @param text the string to search 27 | * @param search the string to search with 28 | * @param position the index to start search at 29 | * @returns boolean, whether or not the text contains the search string. 30 | * @example includes('abcde', 'bcd') // true 31 | */ 32 | export function includes< 33 | T extends string, 34 | S extends string, 35 | P extends number = 0, 36 | >(text: T, search: S, position = 0 as P) { 37 | return text.includes(search, position) as Includes 38 | } 39 | -------------------------------------------------------------------------------- /src/native/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CharAt, 3 | Concat, 4 | EndsWith, 5 | Includes, 6 | Join, 7 | PadEnd, 8 | PadStart, 9 | Repeat, 10 | ReplaceAll, 11 | Replace, 12 | Slice, 13 | Split, 14 | StartsWith, 15 | TrimEnd, 16 | TrimStart, 17 | Trim, 18 | } from '..' 19 | 20 | // biome-ignore lint/complexity/noUselessEmptyExport: 21 | export {} 22 | 23 | declare global { 24 | interface ReadonlyArray { 25 | join( 26 | this: A, 27 | separator?: D 28 | ): Join 29 | } 30 | 31 | interface String { 32 | charAt( 33 | this: S, 34 | index: I 35 | ): CharAt 36 | 37 | concat( 38 | this: S, 39 | ...args: T 40 | ): Concat<[S, ...T]> 41 | 42 | endsWith< 43 | const S extends string, 44 | const T extends string, 45 | P extends number | undefined = undefined, 46 | >(this: S, searchString: T, index?: P): EndsWith 47 | 48 | includes< 49 | const S extends string, 50 | const T extends string, 51 | P extends number = 0, 52 | >(this: S, searchString: T, position?: P): Includes 53 | 54 | padEnd< 55 | const S extends string, 56 | const T extends number, 57 | const U extends string = ' ', 58 | >(this: S, maxLength: T, fillString?: U): PadEnd 59 | 60 | padStart< 61 | const S extends string, 62 | const T extends number, 63 | const U extends string = ' ', 64 | >(this: S, maxLength: T, fillString?: U): PadStart 65 | 66 | repeat( 67 | this: S, 68 | count: T 69 | ): Repeat 70 | 71 | replace< 72 | const S extends string, 73 | const T extends string, 74 | const U extends string, 75 | >(this: S, searchValue: T, replaceValue: U): Replace 76 | 77 | replaceAll< 78 | const S extends string, 79 | const T extends string, 80 | const U extends string, 81 | >(this: S, searchValue: T, replaceValue: U): ReplaceAll 82 | 83 | slice(this: S): Slice 84 | 85 | slice( 86 | this: S, 87 | start?: T 88 | ): Slice 89 | 90 | slice< 91 | const S extends string, 92 | const T extends number, 93 | const U extends number, 94 | >(this: S, start?: T, end?: U): Slice 95 | 96 | split( 97 | this: S, 98 | delimiter: D 99 | ): Split 100 | 101 | startsWith< 102 | const S extends string, 103 | const T extends string, 104 | P extends number = 0, 105 | >(this: S, searchString: T, position?: P): StartsWith 106 | 107 | toLowerCase(this: S): Lowercase 108 | toUpperCase(this: S): Uppercase 109 | 110 | trim(this: S): Trim 111 | 112 | trimEnd(this: S): TrimEnd 113 | 114 | trimStart(this: S): TrimStart 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/native/join.test.ts: -------------------------------------------------------------------------------- 1 | import { type Join, join } from './join.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect< 5 | Equal, 'some nice string'> 6 | > 7 | type test2 = Expect, string>> 8 | type test3 = Expect[], ' '>, string>> 9 | type test4 = Expect, string>> 10 | } 11 | 12 | describe('join', () => { 13 | test('should join words in both type level and runtime level', () => { 14 | const result = join(['a', 'b', 'c'], '-') 15 | expect(result).toEqual('a-b-c') 16 | type test = Expect> 17 | }) 18 | 19 | test('should join only at runtime level when type is wide', () => { 20 | const data = ['a', 'b', 'c'] 21 | const result = join(data, '-') 22 | expect(result).toEqual('a-b-c') 23 | type test = Expect> 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/native/join.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsStringLiteral, 4 | IsStringLiteralArray, 5 | } from '../internal/literals.js' 6 | 7 | /** 8 | * Joins a tuple of strings with the given delimiter. 9 | * T: The tuple of strings to join. 10 | * delimiter: The delimiter. 11 | */ 12 | export type Join< 13 | T extends readonly string[], 14 | delimiter extends string = '', 15 | > = All<[IsStringLiteralArray, IsStringLiteral]> extends true 16 | ? T extends readonly [ 17 | infer first extends string, 18 | ...infer rest extends string[], 19 | ] 20 | ? rest extends [] 21 | ? first 22 | : `${first}${delimiter}${Join}` 23 | : '' 24 | : string 25 | 26 | /** 27 | * A strongly-typed version of `Array.prototype.join`. 28 | * @param tuple the tuple of strings to join. 29 | * @param delimiter the delimiter. 30 | * @returns the joined string in both type level and runtime. 31 | * @example join(['hello', 'world'], '-') // 'hello-world' 32 | */ 33 | export function join( 34 | tuple: T, 35 | delimiter: D = '' as D 36 | ) { 37 | return tuple.join(delimiter) as Join 38 | } 39 | -------------------------------------------------------------------------------- /src/native/length.test.ts: -------------------------------------------------------------------------------- 1 | import { type Length, length } from './length.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, 16>> 5 | type test2 = Expect>, number>> 6 | type test3 = Expect, number>> 7 | } 8 | 9 | describe('length', () => { 10 | test('should return the lenght of a string at both type level and runtime level', () => { 11 | const data = 'some nice string' 12 | const result = length(data) 13 | expect(result).toEqual(16) 14 | type test = Expect> 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/native/length.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../internal/literals.js' 2 | import type { Split } from './split.js' 3 | 4 | /** 5 | * Gets the length of a string. 6 | */ 7 | export type Length = IsStringLiteral extends true 8 | ? Split['length'] 9 | : number 10 | /** 11 | * A strongly-typed version of `String.prototype.length`. 12 | * @param str the string to get the length from. 13 | * @returns the length of the string in both type level and runtime. 14 | * @example length('hello world') // 11 15 | */ 16 | export function length(str: T) { 17 | return str.length as Length 18 | } 19 | -------------------------------------------------------------------------------- /src/native/native-overrides.test.ts: -------------------------------------------------------------------------------- 1 | describe('Array.prototype.join', () => { 2 | test('test 1', () => { 3 | const testArray = ['a', 'b', 'c'] as const 4 | const result = testArray.join(', ') 5 | const expected = 'a, b, c' 6 | expect(result).toEqual(expected) 7 | type test = Expect> 8 | }) 9 | }) 10 | 11 | describe('String.prototype.charAt', () => { 12 | test('test 1', () => { 13 | const result = 'abcdef'.charAt(2) 14 | const expected = 'c' 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | 20 | describe('String.prototype.concat', () => { 21 | test('test 1', () => { 22 | const result = 'abc'.concat('d') 23 | const expected = 'abcd' 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | }) 28 | 29 | describe('String.prototype.endsWith', () => { 30 | test('test 1', () => { 31 | const result = 'abc'.endsWith('c') 32 | const expected = true 33 | expect(result).toEqual(expected) 34 | type test = Expect> 35 | }) 36 | 37 | test('test 2', () => { 38 | const result = 'abc'.endsWith('d') 39 | const expected = false 40 | expect(result).toEqual(expected) 41 | type test = Expect> 42 | }) 43 | 44 | test('test 3', () => { 45 | const result = 'a,b,c'.endsWith('b,c', 5) 46 | const expected = true 47 | expect(result).toEqual(expected) 48 | type test = Expect> 49 | }) 50 | 51 | test('test 4', () => { 52 | const result = 'a,b,c'.endsWith('b,c', 4) 53 | const expected = false 54 | expect(result).toEqual(expected) 55 | type test = Expect> 56 | }) 57 | }) 58 | 59 | describe('String.prototype.includes', () => { 60 | test('test 1', () => { 61 | const result = 'abc'.includes('b') 62 | const expected = true 63 | expect(result).toEqual(expected) 64 | type test = Expect> 65 | }) 66 | 67 | test('test 2', () => { 68 | const result = 'abc'.includes('d') 69 | const expected = false 70 | expect(result).toEqual(expected) 71 | type test = Expect> 72 | }) 73 | 74 | test('test 3', () => { 75 | const result = 'abc'.includes('b', 1) 76 | const expected = true 77 | expect(result).toEqual(expected) 78 | type test = Expect> 79 | }) 80 | 81 | test('test 4', () => { 82 | const result = 'abc'.includes('b', 2) 83 | const expected = false 84 | expect(result).toEqual(expected) 85 | type test = Expect> 86 | }) 87 | }) 88 | 89 | describe('String.prototype.padEnd', () => { 90 | test('test 1', () => { 91 | const result = 'abc'.padEnd(10, '-') 92 | const expected = 'abc-------' 93 | expect(result).toEqual(expected) 94 | type test = Expect> 95 | }) 96 | 97 | test('test 2', () => { 98 | const result = 'abc'.padEnd(10) 99 | const expected = 'abc ' 100 | expect(result).toEqual(expected) 101 | type test = Expect> 102 | }) 103 | }) 104 | 105 | describe('String.prototype.padStart', () => { 106 | test('test 1', () => { 107 | const result = 'abc'.padStart(10, '-') 108 | const expected = '-------abc' 109 | expect(result).toEqual(expected) 110 | type test = Expect> 111 | }) 112 | 113 | test('test 2', () => { 114 | const result = 'abc'.padStart(10) 115 | const expected = ' abc' 116 | expect(result).toEqual(expected) 117 | type test = Expect> 118 | }) 119 | }) 120 | 121 | describe('String.prototype.repeat', () => { 122 | test('test 1', () => { 123 | const result = 'abc'.repeat(3) 124 | const expected = 'abcabcabc' 125 | expect(result).toEqual(expected) 126 | type test = Expect> 127 | }) 128 | 129 | test('test 2', () => { 130 | const result = 'abc'.repeat(0) 131 | const expected = '' 132 | expect(result).toEqual(expected) 133 | type test = Expect> 134 | }) 135 | }) 136 | 137 | describe('String.prototype.slice', () => { 138 | test('test 1', () => { 139 | const result = 'abcdef'.slice(2, 4) 140 | const expected = 'cd' 141 | expect(result).toEqual(expected) 142 | type test = Expect> 143 | }) 144 | 145 | test('test 2', () => { 146 | const result = 'abcdef'.slice(3) 147 | const expected = 'def' 148 | expect(result).toEqual(expected) 149 | type test = Expect> 150 | }) 151 | 152 | test('test 3', () => { 153 | const result = 'abcdef'.slice() 154 | const expected = 'abcdef' 155 | expect(result).toEqual(expected) 156 | type test = Expect> 157 | }) 158 | }) 159 | 160 | describe('String.prototype.replace', () => { 161 | test('test 1', () => { 162 | const result = 'a.b.c'.replace('.', ',') 163 | const expected = 'a,b.c' 164 | expect(result).toEqual(expected) 165 | type test = Expect> 166 | }) 167 | }) 168 | 169 | describe('String.prototype.replaceAll', () => { 170 | test('test 1', () => { 171 | const result = 'a.b.c'.replaceAll('.', ',') 172 | const expected = 'a,b,c' 173 | expect(result).toEqual(expected) 174 | type test = Expect> 175 | }) 176 | }) 177 | 178 | describe('String.prototype.slice', () => { 179 | test('test 1', () => { 180 | const result = 'abcdef'.slice(2, 4) 181 | const expected = 'cd' 182 | expect(result).toEqual(expected) 183 | type test = Expect> 184 | }) 185 | 186 | test('test 2', () => { 187 | const result = 'abcdef'.slice(3) 188 | const expected = 'def' 189 | expect(result).toEqual(expected) 190 | type test = Expect> 191 | }) 192 | 193 | test('test 3', () => { 194 | const result = 'abcdef'.slice() 195 | const expected = 'abcdef' 196 | expect(result).toEqual(expected) 197 | type test = Expect> 198 | }) 199 | }) 200 | 201 | describe('String.prototype.split', () => { 202 | test('test 1', () => { 203 | const result = 'a,b,c'.split(',') 204 | const expected = ['a', 'b', 'c'] as const 205 | type Mutable = [...T] 206 | type Expected = Mutable 207 | expect(result).toEqual(expected) 208 | type test = Expect> 209 | }) 210 | }) 211 | 212 | describe('String.prototype.startsWith', () => { 213 | test('test 1', () => { 214 | const result = 'abc'.startsWith('a') 215 | const expected = true 216 | expect(result).toEqual(expected) 217 | type test = Expect> 218 | }) 219 | 220 | test('test 2', () => { 221 | const result = 'abc'.startsWith('b') 222 | const expected = false 223 | expect(result).toEqual(expected) 224 | type test = Expect> 225 | }) 226 | 227 | test('test 3', () => { 228 | const result = 'abc'.startsWith('b', 1) 229 | const expected = true 230 | expect(result).toEqual(expected) 231 | type test = Expect> 232 | }) 233 | 234 | test('test 4', () => { 235 | const result = 'abc'.startsWith('b', 2) 236 | const expected = false 237 | expect(result).toEqual(expected) 238 | type test = Expect> 239 | }) 240 | }) 241 | 242 | describe('String.prototype.toLowerCase', () => { 243 | test('test 1', () => { 244 | const result = 'ABC'.toLowerCase() 245 | const expected = 'abc' 246 | expect(result).toEqual(expected) 247 | type test = Expect> 248 | }) 249 | }) 250 | 251 | describe('String.prototype.toUpperCase', () => { 252 | test('test 1', () => { 253 | const result = 'abc'.toUpperCase() 254 | const expected = 'ABC' 255 | expect(result).toEqual(expected) 256 | type test = Expect> 257 | }) 258 | }) 259 | 260 | describe('String.prototype.trim', () => { 261 | test('test 1', () => { 262 | const result = ' foo '.trim() 263 | const expected = 'foo' 264 | expect(result).toEqual(expected) 265 | type test = Expect> 266 | }) 267 | }) 268 | 269 | describe('String.prototype.trimEnd', () => { 270 | test('test 1', () => { 271 | const result = ' foo '.trimEnd() 272 | const expected = ' foo' 273 | expect(result).toEqual(expected) 274 | type test = Expect> 275 | }) 276 | }) 277 | 278 | describe('String.prototype.trimStart', () => { 279 | test('test 1', () => { 280 | const result = ' foo '.trimStart() 281 | const expected = 'foo ' 282 | expect(result).toEqual(expected) 283 | type test = Expect> 284 | }) 285 | }) 286 | -------------------------------------------------------------------------------- /src/native/pad-end.test.ts: -------------------------------------------------------------------------------- 1 | import { type PadEnd, padEnd } from './pad-end.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, 'hello '>> 5 | type test2 = Expect, string>> 6 | type test3 = Expect, 10, ' '>, string>> 7 | type test4 = Expect, string>> 8 | type test5 = Expect, string>> 9 | } 10 | 11 | describe('padEnd', () => { 12 | test('should pad a string at the end', () => { 13 | const data = 'hello' 14 | const result = padEnd(data, 10) 15 | expect(result).toEqual('hello ') 16 | type test = Expect> 17 | }) 18 | 19 | test('should pad with a given string', () => { 20 | const data = 'hello' 21 | const result = padEnd(data, 10, '=>') 22 | expect(result).toEqual('hello=>=>=') 23 | type test = Expect=>='>> 24 | }) 25 | 26 | test('should not pad if no arguments are given', () => { 27 | const data = 'hello' 28 | const result = padEnd(data) 29 | expect(result).toEqual('hello') 30 | type test = Expect> 31 | }) 32 | 33 | test('should not pad or truncate if length is shorter than string', () => { 34 | const data = 'hello' 35 | const result = padEnd(data, 3, '=') 36 | expect(result).toEqual('hello') 37 | type test = Expect> 38 | }) 39 | 40 | test('should not pad for negative numbers', () => { 41 | const data = 'hello' 42 | const result = padEnd(data, -1, '=') 43 | expect(result).toEqual('hello') 44 | type test = Expect> 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/native/pad-end.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsNumberLiteral, 4 | IsStringLiteral, 5 | } from '../internal/literals.js' 6 | import type { Math } from '../internal/math.js' 7 | import type { Length } from './length.js' 8 | import type { Repeat } from './repeat.js' 9 | import type { Slice } from './slice.js' 10 | 11 | /** 12 | * Pads a string at the end with another string. 13 | * T: The string to pad. 14 | * times: The number of times to pad. 15 | * pad: The string to pad with. 16 | */ 17 | export type PadEnd< 18 | T extends string, 19 | times extends number = 0, 20 | pad extends string = ' ', 21 | > = All<[IsStringLiteral, IsNumberLiteral]> extends true 22 | ? Math.IsNegative extends false 23 | ? Math.Subtract> extends infer missing extends number 24 | ? `${T}${Slice, 0, missing>}` 25 | : never 26 | : T 27 | : string 28 | 29 | /** 30 | * A strongly-typed version of `String.prototype.padEnd`. 31 | * @param str the string to pad. 32 | * @param length the length to pad. 33 | * @param pad the string to pad with. 34 | * @returns the padded string in both type level and runtime. 35 | * @example padEnd('hello', 10, '=') // 'hello=====' 36 | */ 37 | export function padEnd< 38 | T extends string, 39 | N extends number = 0, 40 | U extends string = ' ', 41 | >(str: T, length: N = 0 as N, pad: U = ' ' as U) { 42 | return str.padEnd(length, pad) as PadEnd 43 | } 44 | -------------------------------------------------------------------------------- /src/native/pad-start.test.ts: -------------------------------------------------------------------------------- 1 | import { type PadStart, padStart } from './pad-start.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, ' hello'>> 5 | type test2 = Expect, string>> 6 | type test3 = Expect, 10, ' '>, string>> 7 | type test4 = Expect, string>> 8 | type test5 = Expect, string>> 9 | } 10 | 11 | describe('padStart', () => { 12 | test('should pad a string at the start', () => { 13 | const data = 'hello' 14 | const result = padStart(data, 10) 15 | expect(result).toEqual(' hello') 16 | type test = Expect> 17 | }) 18 | 19 | test('should pad with a given string', () => { 20 | const data = 'hello' 21 | const result = padStart(data, 10, '=>') 22 | expect(result).toEqual('=>=>=hello') 23 | type test = Expect=>=hello'>> 24 | }) 25 | 26 | test('should not pad if no arguments are given', () => { 27 | const data = 'hello' 28 | const result = padStart(data) 29 | expect(result).toEqual('hello') 30 | type test = Expect> 31 | }) 32 | 33 | test('should not pad or truncate if length is shorter than string', () => { 34 | const data = 'hello' 35 | const result = padStart(data, 3, '=') 36 | expect(result).toEqual('hello') 37 | type test = Expect> 38 | }) 39 | 40 | test('should not pad for negative numbers', () => { 41 | const data = 'hello' 42 | const result = padStart(data, -1, '=') 43 | expect(result).toEqual('hello') 44 | type test = Expect> 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/native/pad-start.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsNumberLiteral, 4 | IsStringLiteral, 5 | } from '../internal/literals.js' 6 | import type { Math } from '../internal/math.js' 7 | import type { Length } from './length.js' 8 | import type { Repeat } from './repeat.js' 9 | import type { Slice } from './slice.js' 10 | 11 | /** 12 | * Pads a string at the start with another string. 13 | * T: The string to pad. 14 | * times: The number of times to pad. 15 | * pad: The string to pad with. 16 | */ 17 | export type PadStart< 18 | T extends string, 19 | times extends number = 0, 20 | pad extends string = ' ', 21 | > = All<[IsStringLiteral, IsNumberLiteral]> extends true 22 | ? Math.IsNegative extends false 23 | ? Math.Subtract> extends infer missing extends number 24 | ? `${Slice, 0, missing>}${T}` 25 | : never 26 | : T 27 | : string 28 | /** 29 | * A strongly-typed version of `String.prototype.padStart`. 30 | * @param str the string to pad. 31 | * @param length the length to pad. 32 | * @param pad the string to pad with. 33 | * @returns the padded string in both type level and runtime. 34 | * @example padStart('hello', 10, '=') // '=====hello' 35 | */ 36 | export function padStart< 37 | T extends string, 38 | N extends number = 0, 39 | U extends string = ' ', 40 | >(str: T, length: N = 0 as N, pad: U = ' ' as U) { 41 | return str.padStart(length, pad) as PadStart 42 | } 43 | -------------------------------------------------------------------------------- /src/native/repeat.test.ts: -------------------------------------------------------------------------------- 1 | import { type Repeat, repeat } from './repeat.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, ' '>> 5 | type test2 = Expect, string>> 6 | type test3 = Expect, 3>, string>> 7 | type test4 = Expect, string>> 8 | } 9 | 10 | describe('repeat', () => { 11 | test('should repeat the string by a given number of times', () => { 12 | const data = 'abc' 13 | const result = repeat(data, 3) 14 | expect(result).toEqual('abcabcabc') 15 | type test = Expect> 16 | }) 17 | 18 | test('should be empty when repeating 0 times', () => { 19 | const data = 'abc' 20 | const result = repeat(data) 21 | expect(result).toEqual('') 22 | type test = Expect> 23 | }) 24 | 25 | test('should throw when trying to repeat with negative number', () => { 26 | const data = 'abc' 27 | expect(() => repeat(data, -1)).toThrow() 28 | type test = Expect, never>> 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/native/repeat.ts: -------------------------------------------------------------------------------- 1 | import type { TupleOf } from '../internal/internals.js' 2 | import type { Math } from '../internal/math.js' 3 | import type { Join } from './join.js' 4 | 5 | import type { 6 | All, 7 | IsNumberLiteral, 8 | IsStringLiteral, 9 | } from '../internal/literals.js' 10 | 11 | /** 12 | * Repeats a string N times. 13 | * T: The string to repeat. 14 | * N: The number of times to repeat. 15 | */ 16 | export type Repeat = All< 17 | [IsStringLiteral, IsNumberLiteral] 18 | > extends true 19 | ? times extends 0 20 | ? '' 21 | : Math.IsNegative extends false 22 | ? Join> 23 | : never 24 | : string 25 | 26 | /** 27 | * A strongly-typed version of `String.prototype.repeat`. 28 | * @param str the string to repeat. 29 | * @param times the number of times to repeat. 30 | * @returns the repeated string in both type level and runtime. 31 | * @example repeat('hello', 3) // 'hellohellohello' 32 | */ 33 | export function repeat( 34 | str: T, 35 | times: N = 0 as N 36 | ) { 37 | return str.repeat(times) as Repeat 38 | } 39 | -------------------------------------------------------------------------------- /src/native/replace-all.test.ts: -------------------------------------------------------------------------------- 1 | import { type ReplaceAll, replaceAll } from './replace-all.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect< 5 | Equal, 'some-nice-string'> 6 | > 7 | type test2 = Expect< 8 | Equal, string> 9 | > 10 | type test3 = Expect, string>> 11 | type test4 = Expect, ' ', '-'>, string>> 12 | type test5 = Expect< 13 | Equal, string> 14 | > 15 | type test6 = Expect< 16 | Equal, string> 17 | > 18 | } 19 | 20 | beforeEach(() => { 21 | vi.clearAllMocks() 22 | }) 23 | 24 | describe('replaceAll', () => { 25 | test('should replace all chars in a string at both type level and runtime level once', () => { 26 | const data = 'some nice string' 27 | const result = replaceAll(data, ' ') 28 | expect(result).toEqual('somenicestring') 29 | type test = Expect> 30 | }) 31 | 32 | test('accepts an argument for the replacement', () => { 33 | const data = 'some nice string' 34 | const result = replaceAll(data, ' ', '@') 35 | expect(result).toEqual('some@nice@string') 36 | type test = Expect> 37 | }) 38 | 39 | test('should replace chars but not at type level when using RegExp', () => { 40 | const data = 'some nice string' 41 | const result = replaceAll(data, / /g, '-') 42 | expect(result).toEqual('some-nice-string') 43 | // Note: `string` instead of `some-nice-string` 44 | type test = Expect> 45 | }) 46 | }) 47 | 48 | describe('replaceAll polyfill', () => { 49 | const replaceAllPlaceholder = String.prototype.replaceAll 50 | 51 | beforeAll(() => { 52 | // @ts-ignore 53 | String.prototype.replaceAll = undefined 54 | }) 55 | 56 | afterAll(() => { 57 | String.prototype.replaceAll = replaceAllPlaceholder 58 | }) 59 | 60 | test('it works through a polyfill', () => { 61 | const spy = vi.spyOn(String.prototype, 'replace') 62 | const data = 'some nice string' 63 | const result = replaceAll(data, ' ', '@') 64 | expect(result).toEqual('some@nice@string') 65 | expect(spy).toHaveBeenCalledWith(/ /g, '@') 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/native/replace-all.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../internal/literals.js' 2 | 3 | /** 4 | * Replaces all the occurrences of a string with another string. 5 | * sentence: The sentence to replace. 6 | * lookup: The lookup string to be replaced. 7 | * replacement: The replacement string. 8 | */ 9 | export type ReplaceAll< 10 | sentence extends string, 11 | lookup extends string | RegExp, 12 | replacement extends string = '', 13 | > = lookup extends string 14 | ? IsStringLiteral extends true 15 | ? sentence extends `${infer rest}${lookup}${infer rest2}` 16 | ? `${rest}${replacement}${ReplaceAll}` 17 | : sentence 18 | : string 19 | : string // Regex used, can't preserve literal 20 | 21 | /** 22 | * A strongly-typed version of `String.prototype.replaceAll`. 23 | * @param sentence the sentence to replace. 24 | * @param lookup the lookup string to be replaced. 25 | * @param replacement the replacement string. 26 | * @returns the replaced string in both type level and runtime. 27 | * @example replaceAll('hello world', 'l', '1') // 'he11o wor1d' 28 | */ 29 | export function replaceAll< 30 | T extends string, 31 | S extends string | RegExp, 32 | R extends string = '', 33 | >(sentence: T, lookup: S, replacement: R = '' as R) { 34 | // Only supported in ES2021+ 35 | if (typeof sentence.replaceAll === 'function') { 36 | return sentence.replaceAll(lookup, replacement) as ReplaceAll 37 | } 38 | 39 | const regex = new RegExp(lookup, 'g') 40 | return sentence.replace(regex, replacement) as ReplaceAll 41 | } 42 | -------------------------------------------------------------------------------- /src/native/replace.test.ts: -------------------------------------------------------------------------------- 1 | import { type Replace, replace } from './replace.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect< 5 | Equal, 'some-nice string'> 6 | > 7 | type test2 = Expect, string>> 8 | type test3 = Expect, string>> 9 | type test4 = Expect, ' ', '-'>, string>> 10 | type test5 = Expect, string>> 11 | type test6 = Expect, string>> 12 | } 13 | 14 | describe('replace', () => { 15 | test('should replace chars in a string at both type level and runtime level once', () => { 16 | const data = 'some nice string' 17 | const result = replace(data, ' ') 18 | expect(result).toEqual('somenice string') 19 | type test = Expect> 20 | }) 21 | test('should replace chars but not at type level when using RegExp', () => { 22 | const data = 'some nice string' 23 | const result = replace(data, /nice /) 24 | expect(result).toEqual('some string') 25 | type test = Expect> 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/native/replace.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../internal/literals.js' 2 | 3 | /** 4 | * Replaces the first occurrence of a string with another string. 5 | * sentence: The sentence to replace. 6 | * lookup: The lookup string to be replaced. 7 | * replacement: The replacement string. 8 | */ 9 | export type Replace< 10 | sentence extends string, 11 | lookup extends string | RegExp, 12 | replacement extends string = '', 13 | > = lookup extends string 14 | ? IsStringLiteral extends true 15 | ? sentence extends `${infer rest}${lookup}${infer rest2}` 16 | ? `${rest}${replacement}${rest2}` 17 | : sentence 18 | : string 19 | : string // Regex used, can't preserve literal 20 | /** 21 | * A strongly-typed version of `String.prototype.replace`. 22 | * @param sentence the sentence to replace. 23 | * @param lookup the lookup string to be replaced. 24 | * @param replacement the replacement string. 25 | * @returns the replaced string in both type level and runtime. 26 | * @example replace('hello world', 'l', '1') // 'he1lo world' 27 | */ 28 | export function replace< 29 | T extends string, 30 | S extends string | RegExp, 31 | R extends string = '', 32 | >(sentence: T, lookup: S, replacement: R = '' as R) { 33 | return sentence.replace(lookup, replacement) as Replace 34 | } 35 | -------------------------------------------------------------------------------- /src/native/slice.test.ts: -------------------------------------------------------------------------------- 1 | import { type Slice, slice } from './slice.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, 'nice string'>> 5 | type test2 = Expect, 'nice'>> 6 | type test3 = Expect, string>> 7 | type test4 = Expect, 5, 9>, string>> 8 | type test5 = Expect, string>> 9 | type test6 = Expect, string>> 10 | type test7 = Expect, 'cde'>> 11 | type test8 = Expect, ''>> 12 | type test9 = Expect, ''>> 13 | 14 | // Template literals 15 | type testTS1 = Expect, 'bc'>> 16 | type testTS2 = Expect, 'bc'>> 17 | type testTS3 = Expect, 'abc'>> 18 | type testTS4 = Expect, string>> 19 | type testTS5 = Expect, `bc${string}`>> 20 | } 21 | 22 | describe('slice', () => { 23 | const str = 'The quick brown fox jumps over the lazy dog.' 24 | test('should slice a string from a startIndex position', () => { 25 | const result = slice(str, 31) 26 | expect(result).toEqual('the lazy dog.') 27 | type test = Expect> 28 | }) 29 | 30 | test('should slice a string from a startIndex to an endIndex position', () => { 31 | const result = slice(str, 4, 19) 32 | expect(result).toEqual('quick brown fox') 33 | type test = Expect> 34 | }) 35 | 36 | test('should slice a string from the end with a negative startIndex', () => { 37 | const result = slice(str, -4) 38 | expect(result).toEqual('dog.') 39 | type test = Expect> 40 | }) 41 | 42 | test('should allow a negative endIndex', () => { 43 | const result = slice(str, 0, -5) 44 | expect(result).toEqual('The quick brown fox jumps over the lazy') 45 | type test = Expect< 46 | Equal 47 | > 48 | }) 49 | 50 | test('should slice a string from the end with a negative startIndex to a negative endIndex', () => { 51 | const result = slice(str, -9, -5) 52 | expect(result).toEqual('lazy') 53 | type test = Expect> 54 | }) 55 | 56 | test('should return an empty string if endIndex is lower than startIndex', () => { 57 | const result = slice(str, -9, -10) 58 | expect(result).toEqual('') 59 | type test = Expect> 60 | 61 | const result2 = slice(str, 9, 1) 62 | expect(result2).toEqual('') 63 | type test2 = Expect> 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/native/slice.ts: -------------------------------------------------------------------------------- 1 | import type { IsNumberLiteral, IsStringLiteral } from '../internal/literals.js' 2 | import type { Math } from '../internal/math.js' 3 | 4 | /** 5 | * Slices a string from a startIndex to an endIndex. 6 | * T: The string to slice. 7 | * startIndex: The start index. 8 | * endIndex: The end index. 9 | */ 10 | export type Slice< 11 | T extends string, 12 | startIndex extends number = 0, 13 | endIndex extends number | undefined = undefined, 14 | > = endIndex extends number 15 | ? _Slice 16 | : _SliceStart 17 | 18 | /** Slice with startIndex and endIndex */ 19 | type _Slice< 20 | T extends string, 21 | startIndex extends number, 22 | endIndex extends number, 23 | _result extends string = '', 24 | > = IsNumberLiteral extends true 25 | ? T extends `${infer head}${infer rest}` 26 | ? IsStringLiteral extends true 27 | ? startIndex extends 0 28 | ? endIndex extends 0 29 | ? _result 30 | : _Slice< 31 | rest, 32 | 0, 33 | Math.Subtract, 1>, 34 | `${_result}${head}` 35 | > 36 | : _Slice< 37 | rest, 38 | Math.Subtract, 1>, 39 | Math.Subtract, 1>, 40 | _result 41 | > 42 | : startIndex | endIndex extends 0 43 | ? _result 44 | : string // Head is non-literal 45 | : IsStringLiteral extends true // Couldn't be split into head/tail 46 | ? _result // T ran out 47 | : startIndex | endIndex extends 0 48 | ? _result // Eg: Slice<`abc${string}`, 1, 3> -> 'bc' 49 | : string // Head is non-literal 50 | : string 51 | 52 | /** Slice with startIndex only */ 53 | type _SliceStart< 54 | T extends string, 55 | startIndex extends number, 56 | _result extends string = '', 57 | > = IsNumberLiteral extends true 58 | ? T extends `${infer head}${infer rest}` 59 | ? IsStringLiteral extends true 60 | ? startIndex extends 0 61 | ? T 62 | : _SliceStart< 63 | rest, 64 | Math.Subtract, 1>, 65 | _result 66 | > 67 | : string 68 | : IsStringLiteral extends true 69 | ? _result 70 | : startIndex extends 0 71 | ? _result 72 | : string 73 | : string 74 | 75 | /** 76 | * A strongly-typed version of `String.prototype.slice`. 77 | * @param str the string to slice. 78 | * @param start the start index. 79 | * @param end the end index. 80 | * @returns the sliced string in both type level and runtime. 81 | * @example slice('hello world', 6) // 'world' 82 | */ 83 | export function slice< 84 | T extends string, 85 | S extends number = 0, 86 | E extends number | undefined = undefined, 87 | >(str: T, start: S = 0 as S, end: E = undefined as E) { 88 | return str.slice(start, end) as Slice 89 | } 90 | -------------------------------------------------------------------------------- /src/native/split.test.ts: -------------------------------------------------------------------------------- 1 | import { type Split, split } from './split.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect< 5 | Equal, ['some', 'nice', 'string']> 6 | > 7 | type test2 = Expect, string[]>> 8 | type test3 = Expect, ' '>, string[]>> 9 | type test4 = Expect, string[]>> 10 | } 11 | 12 | describe('split', () => { 13 | test('should split a string by a delimiter into an array of substrings', () => { 14 | const data = 'some nice string' 15 | const result = split(data, ' ') 16 | expect(result).toEqual(['some', 'nice', 'string']) 17 | type test = Expect> 18 | }) 19 | 20 | test('should no add extra characters when splitting by empty string', () => { 21 | const data = 'hello' 22 | const result = split(data, '') 23 | expect(result).toEqual(['h', 'e', 'l', 'l', 'o']) 24 | type test = Expect> 25 | 26 | const result2 = split('', '') 27 | expect(result2).toEqual([]) 28 | type test2 = Expect> 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/native/split.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../internal/literals.js' 2 | 3 | /** 4 | * Splits a string into an array of substrings. 5 | * T: The string to split. 6 | * delimiter: The delimiter. 7 | */ 8 | export type Split< 9 | T extends string, 10 | delimiter extends string = '', 11 | > = IsStringLiteral extends true 12 | ? T extends `${infer first}${delimiter}${infer rest}` 13 | ? [first, ...Split] 14 | : T extends '' 15 | ? [] 16 | : [T] 17 | : string[] 18 | /** 19 | * A strongly-typed version of `String.prototype.split`. 20 | * @param str the string to split. 21 | * @param delimiter the delimiter. 22 | * @returns the splitted string in both type level and runtime. 23 | * @example split('hello world', ' ') // ['hello', 'world'] 24 | */ 25 | export function split( 26 | str: T, 27 | delimiter: D = '' as D 28 | ) { 29 | return str.split(delimiter) as Split 30 | } 31 | -------------------------------------------------------------------------------- /src/native/starts-with.test.ts: -------------------------------------------------------------------------------- 1 | import { type StartsWith, startsWith } from './starts-with.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, true>> 5 | type test2 = Expect, true>> 6 | type test3 = Expect, 'a'>, boolean>> 7 | type test4 = Expect, boolean>> 8 | type test5 = Expect, boolean>> 9 | type test6 = Expect, boolean>> 10 | type test7 = Expect, true>> 11 | type test8 = Expect, false>> 12 | type test9 = Expect, true>> 13 | type test10 = Expect, true>> 14 | } 15 | 16 | describe('startsWith', () => { 17 | const text = 'abc' 18 | 19 | describe('without offset', () => { 20 | test('should return true when text starts with search', () => { 21 | const result = startsWith(text, 'a') 22 | expect(result).toEqual(true) 23 | type test = Expect> 24 | }) 25 | test('should return false when text does not start with search', () => { 26 | const result = startsWith(text, 'b') 27 | expect(result).toEqual(false) 28 | type test = Expect> 29 | }) 30 | }) 31 | 32 | describe('with offset', () => { 33 | test('should return true when offset text starts with search', () => { 34 | const result = startsWith(text, 'b', 1) 35 | expect(result).toEqual(true) 36 | type test = Expect> 37 | }) 38 | test('should return false when offset string does not start with search', () => { 39 | const result = startsWith(text, 'a', 1) 40 | expect(result).toEqual(false) 41 | type test = Expect> 42 | }) 43 | }) 44 | 45 | describe('with bad offset', () => { 46 | test('should return true when text starts with search and offset is negative', () => { 47 | const result = startsWith(text, 'a', -1) 48 | expect(result).toEqual(true) 49 | type test = Expect> 50 | }) 51 | test('should return false when offset is greater than text length', () => { 52 | const result = startsWith(text, 'a', 10) 53 | expect(result).toEqual(false) 54 | type test = Expect> 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/native/starts-with.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsNumberLiteral, 4 | IsStringLiteral, 5 | } from '../internal/literals.js' 6 | import type { Math } from '../internal/math.js' 7 | import type { Slice } from './slice.js' 8 | 9 | /** 10 | * Checks if a string starts with another string. 11 | * T: The string to check. 12 | * S: The string to check against. 13 | * P: The position to start the search. 14 | */ 15 | export type StartsWith< 16 | T extends string, 17 | S extends string, 18 | P extends number = 0, 19 | > = All<[IsStringLiteral, IsNumberLiteral

]> extends true 20 | ? Math.IsNegative

extends false 21 | ? P extends 0 22 | ? S extends `${infer SHead}${infer SRest}` 23 | ? T extends `${infer THead}${infer TRest}` 24 | ? IsStringLiteral extends true 25 | ? THead extends SHead 26 | ? StartsWith 27 | : false // Heads weren't equal 28 | : boolean // THead is non-literal 29 | : IsStringLiteral extends true // Couldn't split T 30 | ? false // T ran out, but we still have S 31 | : boolean // T (or TRest) is not a literal 32 | : true // Couldn't split S, we've already ruled out non-literal 33 | : StartsWith, S, 0> // P is >0, slice 34 | : StartsWith // P is negative, ignore it 35 | : boolean 36 | 37 | /** 38 | * A strongly-typed version of `String.prototype.startsWith`. 39 | * @param text the string to search. 40 | * @param search the string to search with. 41 | * @param position the index to start search at. 42 | * @returns boolean, whether or not the text string starts with the search string. 43 | * @example startsWith('abc', 'a') // true 44 | */ 45 | export function startsWith< 46 | T extends string, 47 | S extends string, 48 | P extends number = 0, 49 | >(text: T, search: S, position = 0 as P) { 50 | return text.startsWith(search, position) as StartsWith 51 | } 52 | -------------------------------------------------------------------------------- /src/native/to-lower-case.test.ts: -------------------------------------------------------------------------------- 1 | import { SEPARATORS_TEXT, WEIRD_TEXT } from '../internal/fixtures.js' 2 | import { toLowerCase } from './to-lower-case.js' 3 | 4 | describe('toLowerCase', () => { 5 | test('casing functions', () => { 6 | const expected = 7 | ' someweird-cased$*string1986foo [bar] w_for_wumbo...' as const 8 | const result = toLowerCase(WEIRD_TEXT) 9 | expect(result).toEqual(expected) 10 | type test = Expect> 11 | }) 12 | test('with various separators', () => { 13 | const result = toLowerCase(SEPARATORS_TEXT) 14 | const expected = '[one] two-three/four.five(six){seven}|eight_nine\\ten' 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/native/to-lower-case.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function is a strongly-typed counterpart of String.prototype.toLowerCase. 3 | * @param str the string to make lowercase. 4 | * @returns the lowercased string. 5 | * @example toLowerCase('HELLO WORLD') // 'hello world' 6 | */ 7 | export function toLowerCase(str: T) { 8 | return str.toLowerCase() as Lowercase 9 | } 10 | -------------------------------------------------------------------------------- /src/native/to-upper-case.test.ts: -------------------------------------------------------------------------------- 1 | import { SEPARATORS_TEXT, WEIRD_TEXT } from '../internal/fixtures.js' 2 | import { toUpperCase } from './to-upper-case.js' 3 | 4 | describe('toUpperCase', () => { 5 | test('casing functions', () => { 6 | const expected = 7 | ' SOMEWEIRD-CASED$*STRING1986FOO [BAR] W_FOR_WUMBO...' as const 8 | const result = toUpperCase(WEIRD_TEXT) 9 | expect(result).toEqual(expected) 10 | type test = Expect> 11 | }) 12 | test('with various separators', () => { 13 | const result = toUpperCase(SEPARATORS_TEXT) 14 | const expected = '[ONE] TWO-THREE/FOUR.FIVE(SIX){SEVEN}|EIGHT_NINE\\TEN' 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/native/to-upper-case.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function is a strongly-typed counterpart of String.prototype.toUpperCase. 3 | * @param str the string to make uppercase. 4 | * @returns the uppercased string. 5 | * @example toUpperCase('hello world') // 'HELLO WORLD' 6 | */ 7 | export function toUpperCase(str: T) { 8 | return str.toUpperCase() as Uppercase 9 | } 10 | -------------------------------------------------------------------------------- /src/native/trim-end.test.ts: -------------------------------------------------------------------------------- 1 | import { type TrimEnd, trimEnd } from './trim-end.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, ' some nice string'>> 5 | type test2 = Expect, string>> 6 | type test3 = Expect>, Uppercase>> 7 | type test4 = Expect< 8 | Equal} `>, `on${Capitalize}`> 9 | > 10 | type test5 = Expect, `hey, ${string}`>> 11 | } 12 | 13 | describe('trimEnd', () => { 14 | test('should trim the end of a string at both type level and runtime level', () => { 15 | const data = ' some nice string ' 16 | const result = trimEnd(data) 17 | expect(result).toEqual(' some nice string') 18 | type test = Expect> 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/native/trim-end.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Trims all whitespaces at the end of a string. 3 | * T: The string to trim. 4 | */ 5 | export type TrimEnd = T extends `${infer rest} ` 6 | ? TrimEnd 7 | : T 8 | /** 9 | * A strongly-typed version of `String.prototype.trimEnd`. 10 | * @param str the string to trim. 11 | * @returns the trimmed string in both type level and runtime. 12 | * @example trimEnd(' hello world ') // ' hello world' 13 | */ 14 | export function trimEnd(str: T) { 15 | return str.trimEnd() as TrimEnd 16 | } 17 | -------------------------------------------------------------------------------- /src/native/trim-start.test.ts: -------------------------------------------------------------------------------- 1 | import { type TrimStart, trimStart } from './trim-start.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect< 5 | Equal, 'some nice string '> 6 | > 7 | type test2 = Expect, string>> 8 | type test3 = Expect>, Uppercase>> 9 | type test4 = Expect< 10 | Equal}`>, `on${Capitalize}`> 11 | > 12 | type test5 = Expect, `hey, ${string}`>> 13 | } 14 | 15 | describe('trimStart', () => { 16 | test('should trim the start of a string at both type level and runtime level', () => { 17 | const data = ' some nice string ' 18 | const result = trimStart(data) 19 | expect(result).toEqual('some nice string ') 20 | type test = Expect> 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/native/trim-start.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Trims all whitespaces at the start of a string. 3 | * T: The string to trim. 4 | */ 5 | export type TrimStart = T extends ` ${infer rest}` 6 | ? TrimStart 7 | : T 8 | /** 9 | * A strongly-typed version of `String.prototype.trimStart`. 10 | * @param str the string to trim. 11 | * @returns the trimmed string in both type level and runtime. 12 | * @example trimStart(' hello world ') // 'hello world ' 13 | */ 14 | export function trimStart(str: T) { 15 | return str.trimStart() as TrimStart 16 | } 17 | -------------------------------------------------------------------------------- /src/native/trim.test.ts: -------------------------------------------------------------------------------- 1 | import { type Trim, trim } from './trim.js' 2 | 3 | namespace TypeTests { 4 | type test1 = Expect, 'some nice string'>> 5 | type test2 = Expect, string>> 6 | type test3 = Expect>, Uppercase>> 7 | type test4 = Expect< 8 | Equal} `>, `on${Capitalize}`> 9 | > 10 | } 11 | 12 | describe('trim', () => { 13 | test('should trim a string at both type level and runtime level', () => { 14 | const data = ' some nice string ' 15 | const result = trim(data) 16 | expect(result).toEqual('some nice string') 17 | type test = Expect> 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/native/trim.ts: -------------------------------------------------------------------------------- 1 | import type { TrimEnd } from './trim-end.js' 2 | import type { TrimStart } from './trim-start.js' 3 | 4 | /** 5 | * Trims all whitespaces at the start and end of a string. 6 | * T: The string to trim. 7 | */ 8 | export type Trim = TrimEnd> 9 | 10 | /** 11 | * A strongly-typed version of `String.prototype.trim`. 12 | * @param str the string to trim. 13 | * @returns the trimmed string in both type level and runtime. 14 | * @example trim(' hello world ') // 'hello world' 15 | */ 16 | export function trim(str: T) { 17 | return str.trim() as Trim 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/characters/apostrophe.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../../internal/literals.js' 2 | import { type ReplaceAll, replaceAll } from '../../native/replace-all.js' 3 | 4 | type Apostrophe = "'" 5 | 6 | /** 7 | * Checks if the given character is an apostrophe 8 | */ 9 | export type IsApostrophe = IsStringLiteral extends true 10 | ? T extends Apostrophe 11 | ? true 12 | : false 13 | : boolean 14 | 15 | export type RemoveApostrophe = ReplaceAll 16 | 17 | export function removeApostrophe( 18 | str: T 19 | ): RemoveApostrophe { 20 | return replaceAll(str, "'", '') 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/characters/letters.test.ts: -------------------------------------------------------------------------------- 1 | import type { IsLetter, IsLower, IsUpper } from './letters.js' 2 | 3 | namespace TypeChecks { 4 | type testIsLower1 = Expect, false>> 5 | type testIsLower2 = Expect, true>> 6 | type testIsLower3 = Expect, false>> 7 | type testIsLower4 = Expect, false>> 8 | type testIsLower5 = Expect, boolean>> 9 | 10 | type testIsUpper1 = Expect, false>> 11 | type testIsUpper2 = Expect, false>> 12 | type testIsUpper3 = Expect, true>> 13 | type testIsUpper4 = Expect, false>> 14 | type testIsUpper5 = Expect, boolean>> 15 | 16 | type testIsLetter1 = Expect, false>> 17 | type testIsLetter2 = Expect, true>> 18 | type testIsLetter3 = Expect, true>> 19 | type testIsLetter4 = Expect, false>> 20 | type testIsLetter5 = Expect, boolean>> 21 | } 22 | 23 | test('dummy test', () => expect(true).toBe(true)) 24 | -------------------------------------------------------------------------------- /src/utils/characters/letters.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../../internal/literals.js' 2 | 3 | type UpperChars = 4 | | 'A' 5 | | 'B' 6 | | 'C' 7 | | 'D' 8 | | 'E' 9 | | 'F' 10 | | 'G' 11 | | 'H' 12 | | 'I' 13 | | 'J' 14 | | 'K' 15 | | 'L' 16 | | 'M' 17 | | 'N' 18 | | 'O' 19 | | 'P' 20 | | 'Q' 21 | | 'R' 22 | | 'S' 23 | | 'T' 24 | | 'U' 25 | | 'V' 26 | | 'W' 27 | | 'X' 28 | | 'Y' 29 | | 'Z' 30 | type LowerChars = Lowercase 31 | 32 | // UTILITIES FOR DETECTING CHARS 33 | /** 34 | * Checks if the given character is an upper case letter. 35 | */ 36 | export type IsUpper = IsStringLiteral extends true 37 | ? T extends UpperChars 38 | ? true 39 | : false 40 | : boolean 41 | 42 | /** 43 | * Checks if the given character is a lower case letter. 44 | */ 45 | export type IsLower = IsStringLiteral extends true 46 | ? T extends LowerChars 47 | ? true 48 | : false 49 | : boolean 50 | 51 | /** 52 | * Checks if the given character is a letter. 53 | */ 54 | export type IsLetter = IsStringLiteral extends true 55 | ? T extends LowerChars | UpperChars 56 | ? true 57 | : false 58 | : boolean 59 | -------------------------------------------------------------------------------- /src/utils/characters/numbers.test.ts: -------------------------------------------------------------------------------- 1 | import type { IsDigit } from './numbers.js' 2 | 3 | namespace TypeChecks { 4 | type test1 = Expect, true>> 5 | type test2 = Expect, false>> 6 | type test3 = Expect, false>> 7 | type test4 = Expect, false>> 8 | type test5 = Expect, boolean>> 9 | } 10 | 11 | test('dummy test', () => expect(true).toBe(true)) 12 | -------------------------------------------------------------------------------- /src/utils/characters/numbers.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../../internal/literals.js' 2 | 3 | export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 4 | 5 | /** 6 | * Checks if the given character is a number. 7 | */ 8 | export type IsDigit = IsStringLiteral extends true 9 | ? T extends Digit 10 | ? true 11 | : false 12 | : boolean 13 | -------------------------------------------------------------------------------- /src/utils/characters/separators.test.ts: -------------------------------------------------------------------------------- 1 | import { SEPARATOR_REGEX } from './separators.js' 2 | import type { IsSeparator } from './separators.js' 3 | 4 | namespace TypeChecks { 5 | type test1 = Expect, false>> 6 | type test2 = Expect, false>> 7 | type test3 = Expect, false>> 8 | type test4 = Expect, false>> 9 | type test5 = Expect, true>> 10 | type test6 = Expect, true>> 11 | type test7 = Expect, true>> 12 | type test8 = Expect, true>> 13 | type test9 = Expect, true>> 14 | type test10 = Expect, boolean>> 15 | type test11 = Expect>, boolean>> 16 | } 17 | 18 | describe('SEPARATOR_REGEX', () => { 19 | test('dummy regex test', () => { 20 | expect(SEPARATOR_REGEX.test('[test]')).toEqual(true) 21 | expect(SEPARATOR_REGEX.test('te.st')).toEqual(true) 22 | expect(SEPARATOR_REGEX.test('te$st')).toEqual(false) 23 | expect(SEPARATOR_REGEX.test('test')).toEqual(false) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/utils/characters/separators.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../../internal/literals.js' 2 | 3 | const UNESCAPED_SEPARATORS = [ 4 | '[', 5 | ']', 6 | '{', 7 | '}', 8 | '(', 9 | ')', 10 | '|', 11 | '/', 12 | '-', 13 | '\\', 14 | ] as const 15 | const SEPARATORS = [...UNESCAPED_SEPARATORS, ' ', '_', '.'] as const 16 | 17 | /** Escape characters with special significance in regular expressions */ 18 | function escapeChar(char: string): string { 19 | return (UNESCAPED_SEPARATORS as readonly string[]).includes(char) 20 | ? `\\${char}` 21 | : char 22 | } 23 | 24 | export const SEPARATOR_REGEX = new RegExp( 25 | `[${SEPARATORS.map(escapeChar).join('')}]`, 26 | 'g' 27 | ) 28 | 29 | export type Separator = (typeof SEPARATORS)[number] 30 | 31 | /** 32 | * Checks if the given character is a separator. 33 | * E.g. space, underscore, dash, dot, slash. 34 | */ 35 | export type IsSeparator = IsStringLiteral extends true 36 | ? T extends Separator 37 | ? true 38 | : false 39 | : boolean 40 | -------------------------------------------------------------------------------- /src/utils/characters/special.test.ts: -------------------------------------------------------------------------------- 1 | import type * as Subject from './special.js' 2 | 3 | namespace TypeChecks { 4 | type test1 = Expect, false>> 5 | type test2 = Expect, false>> 6 | type test3 = Expect, false>> 7 | type test4 = Expect, true>> 8 | type test5 = Expect, false>> 9 | type test6 = Expect, true>> 10 | type test7 = Expect, false>> 11 | type test8 = Expect, boolean>> 12 | type test9 = Expect>, boolean>> 13 | } 14 | test('dummy test', () => expect(true).toBe(true)) 15 | -------------------------------------------------------------------------------- /src/utils/characters/special.ts: -------------------------------------------------------------------------------- 1 | import type { IsStringLiteral } from '../../internal/literals.js' 2 | import type { IsApostrophe } from './apostrophe.js' 3 | import type { IsLetter } from './letters.js' 4 | import type { IsDigit } from './numbers.js' 5 | import type { IsSeparator } from './separators.js' 6 | 7 | /** 8 | * Checks if the given character is a special character. 9 | * E.g. not a letter, number, or separator. 10 | */ 11 | export type IsSpecial = IsStringLiteral extends true 12 | ? IsLetter extends true 13 | ? false 14 | : IsDigit extends true 15 | ? false 16 | : IsSeparator extends true 17 | ? false 18 | : IsApostrophe extends true 19 | ? false 20 | : true 21 | : boolean 22 | -------------------------------------------------------------------------------- /src/utils/object-keys/camel-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type CamelKeys, camelKeys } from './camel-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | CamelKeys<{ 7 | 'some-value': { 'deep-nested': true } 8 | 'other-value': true 9 | }>, 10 | { someValue: { 'deep-nested': true }; otherValue: true } 11 | > 12 | > 13 | } 14 | 15 | test('camelKeys', () => { 16 | const expected = { 17 | some: { 'deep-nested': { value: true } }, 18 | otherValue: true, 19 | } 20 | const result = camelKeys({ 21 | some: { 'deep-nested': { value: true } }, 22 | 'other-value': true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/camel-keys.ts: -------------------------------------------------------------------------------- 1 | import { type CamelCase, camelCase } from '../word-case/camel-case.js' 2 | import { transformKeys } from './transform-keys.js' 3 | 4 | /** 5 | * Shallowly transforms the keys of a Record to camelCase. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type CamelKeys = T extends [] 9 | ? T 10 | : { [K in keyof T as CamelCase>]: T[K] } 11 | /** 12 | * A strongly typed function that shallowly transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. 13 | * @param obj the object to transform. 14 | * @returns the transformed object. 15 | * @example camelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { 'fizz-buz': true } } 16 | */ 17 | export function camelKeys(obj: T): CamelKeys { 18 | return transformKeys(obj, camelCase) as never 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/object-keys/constant-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type ConstantKeys, constantKeys } from './constant-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | ConstantKeys<{ 7 | someValue: { deepNested: true } 8 | otherValue: true 9 | }>, 10 | { SOME_VALUE: { deepNested: true }; OTHER_VALUE: true } 11 | > 12 | > 13 | } 14 | 15 | describe('constantKeys', () => { 16 | test('should shollowly transform object keys to constant case', () => { 17 | const expected = { 18 | SOME: { deepNested: { value: true } }, 19 | OTHER_VALUE: true, 20 | } 21 | const result = constantKeys({ 22 | some: { deepNested: { value: true } }, 23 | otherValue: true, 24 | }) 25 | expect(result).toEqual(expected) 26 | type test = Expect> 27 | }) 28 | 29 | test('should handle null properly', () => { 30 | const expected = null 31 | const result = constantKeys(null) 32 | expect(result).toEqual(expected) 33 | type test = Expect> 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/utils/object-keys/constant-keys.ts: -------------------------------------------------------------------------------- 1 | import { type ConstantCase, constantCase } from '../word-case/constant-case.js' 2 | import { transformKeys } from './transform-keys.js' 3 | 4 | /** 5 | * Shallowly transforms the keys of a Record to CONSTANT_CASE. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type ConstantKeys = T extends [] 9 | ? T 10 | : { [K in keyof T as ConstantCase>]: T[K] } 11 | /** 12 | * A strongly typed function that shallowly transforms the keys of an object to CONSTANT_CASE. The transformation is done both at runtime and type level. 13 | * @param obj the object to transform. 14 | * @returns the transformed object. 15 | * @example constantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { 'fizz-buzz': true } } 16 | */ 17 | export function constantKeys(obj: T): ConstantKeys { 18 | return transformKeys(obj, constantCase) as never 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-camel-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type DeepCamelKeys, deepCamelKeys } from './deep-camel-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | DeepCamelKeys<{ 7 | some: { 'deep-nested': { value: true } } 8 | 'other-value': true 9 | }>, 10 | { some: { deepNested: { value: true } }; otherValue: true } 11 | > 12 | > 13 | } 14 | 15 | describe('deepCamelKeys', () => { 16 | test('should camelize the object', () => { 17 | const expected = { 18 | some: { deepNested: { value: true } }, 19 | otherValue: true, 20 | } 21 | const result = deepCamelKeys({ 22 | some: { 'deep-nested': { value: true } }, 23 | 'other-value': true, 24 | }) 25 | expect(result).toEqual(expected) 26 | type test = Expect> 27 | }) 28 | 29 | test('should camelize from SCREAMING_SNAKE_CASE', () => { 30 | const obj = { 31 | NODE_ENV: 'development', 32 | } 33 | const expected = { 34 | nodeEnv: 'development', 35 | } 36 | const result = deepCamelKeys(obj) 37 | expect(result).toEqual(expected) 38 | type test = Expect> 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-camel-keys.ts: -------------------------------------------------------------------------------- 1 | import { type CamelCase, camelCase } from '../word-case/camel-case.js' 2 | import { deepTransformKeys } from './deep-transform-keys.js' 3 | 4 | /** 5 | * Recursively transforms the keys of a Record to camelCase. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type DeepCamelKeys = T extends [any, ...any] 9 | ? { [I in keyof T]: DeepCamelKeys } 10 | : T extends (infer V)[] 11 | ? DeepCamelKeys[] 12 | : { 13 | [K in keyof T as CamelCase>]: DeepCamelKeys 14 | } 15 | /** 16 | * A strongly typed function that recursively transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. 17 | * @param obj the object to transform. 18 | * @returns the transformed object. 19 | * @example deepCamelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { fizzBuzz: true } } 20 | */ 21 | export function deepCamelKeys(obj: T): DeepCamelKeys { 22 | return deepTransformKeys(obj, camelCase) as never 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-constant-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DeepConstantKeys, 3 | deepConstantKeys, 4 | } from './deep-constant-keys.js' 5 | 6 | namespace TypeTransforms { 7 | type test5 = Expect< 8 | Equal< 9 | DeepConstantKeys<{ 10 | some: { 'deep-nested': { value: true } } 11 | 'other-value': true 12 | }>, 13 | { SOME: { DEEP_NESTED: { VALUE: true } }; OTHER_VALUE: true } 14 | > 15 | > 16 | } 17 | 18 | describe('deepConstantKeys', () => { 19 | test('should deeply transform object keys to constant case', () => { 20 | const expected = { 21 | SOME: { DEEP_NESTED: { VALUE: true } }, 22 | OTHER_VALUE: true, 23 | } 24 | const result = deepConstantKeys({ 25 | some: { deepNested: { value: true } }, 26 | otherValue: true, 27 | }) 28 | expect(result).toEqual(expected) 29 | type test = Expect> 30 | }) 31 | 32 | test('should handle null properly', () => { 33 | const expected = null 34 | const result = deepConstantKeys(null) 35 | expect(result).toEqual(expected) 36 | type test = Expect> 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-constant-keys.ts: -------------------------------------------------------------------------------- 1 | import { type ConstantCase, constantCase } from '../word-case/constant-case.js' 2 | import { deepTransformKeys } from './deep-transform-keys.js' 3 | 4 | /** 5 | * Recursively transforms the keys of a Record to CONSTANT_CASE. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type DeepConstantKeys = T extends [any, ...any] 9 | ? { [I in keyof T]: DeepConstantKeys } 10 | : T extends (infer V)[] 11 | ? DeepConstantKeys[] 12 | : { 13 | [K in keyof T as ConstantCase>]: DeepConstantKeys< 14 | T[K] 15 | > 16 | } 17 | /** 18 | * A strongly typed function that recursively transforms the keys of an object to CONSTANT_CASE. The transformation is done both at runtime and type level. 19 | * @param obj the object to transform. 20 | * @returns the transformed object. 21 | * @example deepConstantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { FIZZ_BUZZ: true } } 22 | */ 23 | export function deepConstantKeys(obj: T): DeepConstantKeys { 24 | return deepTransformKeys(obj, constantCase) as never 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-delimiter-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DeepDelimiterKeys, 3 | deepDelimiterKeys, 4 | } from './deep-delimiter-keys.js' 5 | 6 | namespace TypeTransforms { 7 | type test = Expect< 8 | Equal< 9 | DeepDelimiterKeys< 10 | { 11 | some: { 'deep-nested': { value: true } } 12 | 'other-value': true 13 | }, 14 | '@' 15 | >, 16 | { some: { 'deep@nested': { value: true } }; 'other@value': true } 17 | > 18 | > 19 | } 20 | 21 | test('deepDelimiterKeys', () => { 22 | const expected = { 23 | some: { 'deep@nested': { value: true } }, 24 | 'other@value': true, 25 | } 26 | const result = deepDelimiterKeys( 27 | { 28 | some: { 'deep-nested': { value: true } }, 29 | 'other-value': true, 30 | }, 31 | '@' 32 | ) 33 | expect(result).toEqual(expected) 34 | type test = Expect> 35 | }) 36 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-delimiter-keys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DelimiterCase, 3 | delimiterCase, 4 | } from '../word-case/delimiter-case.js' 5 | import { deepTransformKeys } from './deep-transform-keys.js' 6 | 7 | /** 8 | * Recursively transforms the keys of a Record to a custom delimiter case. 9 | * T: the type of the Record to transform. 10 | * D: the delimiter to use. 11 | */ 12 | export type DeepDelimiterKeys = T extends [any, ...any] 13 | ? { [I in keyof T]: DeepDelimiterKeys } 14 | : T extends (infer V)[] 15 | ? DeepDelimiterKeys[] 16 | : { 17 | [K in keyof T as DelimiterCase< 18 | Extract, 19 | D 20 | >]: DeepDelimiterKeys 21 | } 22 | /** 23 | * A strongly typed function that recursively transforms the keys of an object to a custom delimiter case. The transformation is done both at runtime and type level. 24 | * @param obj the object to transform. 25 | * @param delimiter the delimiter to use. 26 | * @returns the transformed object. 27 | * @example deepDelimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } 28 | */ 29 | export function deepDelimiterKeys( 30 | obj: T, 31 | delimiter: D 32 | ): DeepDelimiterKeys { 33 | return deepTransformKeys(obj, (str) => delimiterCase(str, delimiter)) as never 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-kebab-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type DeepKebabKeys, deepKebabKeys } from './deep-kebab-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | DeepKebabKeys<{ 7 | some: { deepNested: { value: true } } 8 | otherValue: true 9 | }>, 10 | { some: { 'deep-nested': { value: true } }; 'other-value': true } 11 | > 12 | > 13 | } 14 | 15 | test('deepKebabKeys', () => { 16 | const expected = { 17 | some: { 'deep-nested': { value: true } }, 18 | 'other-value': true, 19 | } 20 | const result = deepKebabKeys({ 21 | some: { deepNested: { value: true } }, 22 | otherValue: true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-kebab-keys.ts: -------------------------------------------------------------------------------- 1 | import { type KebabCase, kebabCase } from '../word-case/kebab-case.js' 2 | import { deepTransformKeys } from './deep-transform-keys.js' 3 | 4 | /** 5 | * Recursively transforms the keys of a Record to kebab-case. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type DeepKebabKeys = T extends [any, ...any] 9 | ? { [I in keyof T]: DeepKebabKeys } 10 | : T extends (infer V)[] 11 | ? DeepKebabKeys[] 12 | : { 13 | [K in keyof T as KebabCase>]: DeepKebabKeys 14 | } 15 | /** 16 | * A strongly typed function that recursively transforms the keys of an object to kebab-case. The transformation is done both at runtime and type level. 17 | * @param obj the object to transform. 18 | * @returns the transformed object. 19 | * @example deepKebabKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo-bar': { 'fizz-buzz': true } } 20 | */ 21 | export function deepKebabKeys(obj: T): DeepKebabKeys { 22 | return deepTransformKeys(obj, kebabCase) as never 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-pascal-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type DeepPascalKeys, deepPascalKeys } from './deep-pascal-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | DeepPascalKeys<{ 7 | some: { 'deep-nested': { value: true } } 8 | 'other-value': true 9 | }>, 10 | { Some: { DeepNested: { Value: true } }; OtherValue: true } 11 | > 12 | > 13 | } 14 | 15 | test('deepPascalKeys', () => { 16 | const expected = { 17 | Some: { DeepNested: { Value: true } }, 18 | OtherValue: true, 19 | } 20 | const result = deepPascalKeys({ 21 | some: { deepNested: { value: true } }, 22 | otherValue: true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-pascal-keys.ts: -------------------------------------------------------------------------------- 1 | import { type PascalCase, pascalCase } from '../word-case/pascal-case.js' 2 | import { deepTransformKeys } from './deep-transform-keys.js' 3 | 4 | /** 5 | * Recursively transforms the keys of a Record to PascalCase. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type DeepPascalKeys = T extends [any, ...any] 9 | ? { [I in keyof T]: DeepPascalKeys } 10 | : T extends (infer V)[] 11 | ? DeepPascalKeys[] 12 | : { 13 | [K in keyof T as PascalCase>]: DeepPascalKeys 14 | } 15 | /** 16 | * A strongly typed function that recursively transforms the keys of an object to pascal case. The transformation is done both at runtime and type level. 17 | * @param obj the object to transform. 18 | * @returns the transformed object. 19 | * @example deepPascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { FizzBuzz: true } } 20 | */ 21 | export function deepPascalKeys(obj: T): DeepPascalKeys { 22 | return deepTransformKeys(obj, pascalCase) as never 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-snake-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type DeepSnakeKeys, deepSnakeKeys } from './deep-snake-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | DeepSnakeKeys<{ 7 | some: { 'deep-nested': { value: true } } 8 | 'other-value': true 9 | }>, 10 | { some: { deep_nested: { value: true } }; other_value: true } 11 | > 12 | > 13 | } 14 | 15 | test('deepSnakeKeys', () => { 16 | const expected = { 17 | some: { deep_nested: { value: true } }, 18 | other_value: true, 19 | } 20 | const result = deepSnakeKeys({ 21 | some: { deepNested: { value: true } }, 22 | otherValue: true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-snake-keys.ts: -------------------------------------------------------------------------------- 1 | import { type SnakeCase, snakeCase } from '../word-case/snake-case.js' 2 | import { deepTransformKeys } from './deep-transform-keys.js' 3 | 4 | /** 5 | * Recursively transforms the keys of a Record to snake_case. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type DeepSnakeKeys = T extends [any, ...any] 9 | ? { [I in keyof T]: DeepSnakeKeys } 10 | : T extends (infer V)[] 11 | ? DeepSnakeKeys[] 12 | : { 13 | [K in keyof T as SnakeCase>]: DeepSnakeKeys 14 | } 15 | /** 16 | * A strongly typed function that recursively transforms the keys of an object to snake_case. The transformation is done both at runtime and type level. 17 | * @param obj the object to transform. 18 | * @returns the transformed object. 19 | * @example deepSnakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz_buzz': true } } 20 | */ 21 | export function deepSnakeKeys(obj: T): DeepSnakeKeys { 22 | return deepTransformKeys(obj, snakeCase) as never 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-transform-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { deepTransformKeys } from './deep-transform-keys.js' 2 | 3 | describe('deepTransformKeys', () => { 4 | test('should deeply transform the keys of an object', () => { 5 | const expected = { 6 | SOME: [{ 'DEEP-NESTED': { VALUE: true } }], 7 | 'OTHER-VALUE': true, 8 | } 9 | const result = deepTransformKeys( 10 | { 11 | some: [{ 'deep-nested': { value: true } }], 12 | 'other-value': true, 13 | }, 14 | (key) => key.toUpperCase() 15 | ) 16 | expect(result).toEqual(expected) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/utils/object-keys/deep-transform-keys.ts: -------------------------------------------------------------------------------- 1 | import { typeOf } from '../../internal/internals.js' 2 | 3 | /** 4 | * This function is used to transform the keys of an object deeply. 5 | * It will only be transformed at runtime, so it's not type safe. 6 | * @param obj the object to transform. 7 | * @param transform the function to transform the keys from string to string. 8 | * @returns the transformed object. 9 | * @example deepTransformKeys({ 'foo-bar': { 'fizz-buzz': true } }, camelCase) 10 | * // { fooBar: { fizzBuzz: true } } 11 | */ 12 | export function deepTransformKeys( 13 | obj: T, 14 | transform: (s: string) => string 15 | ): T { 16 | if (!['object', 'array'].includes(typeOf(obj))) return obj 17 | 18 | if (Array.isArray(obj)) { 19 | return obj.map((x) => deepTransformKeys(x, transform)) as T 20 | } 21 | const res = {} as T 22 | for (const key in obj) { 23 | res[transform(key) as keyof T] = deepTransformKeys(obj[key], transform) 24 | } 25 | return res 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/delimiter-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type DelimiterKeys, delimiterKeys } from './delimiter-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | DelimiterKeys< 7 | { 8 | 'some-value': { 'nested-value': true } 9 | 'other-value': true 10 | }, 11 | '@' 12 | >, 13 | { 'some@value': { 'nested-value': true }; 'other@value': true } 14 | > 15 | > 16 | } 17 | 18 | test('delimiterKeys', () => { 19 | const expected = { 20 | some: { 'deep-nested': { value: true } }, 21 | 'other@value': true, 22 | } 23 | const result = delimiterKeys( 24 | { 25 | some: { 'deep-nested': { value: true } }, 26 | 'other-value': true, 27 | }, 28 | '@' 29 | ) 30 | expect(result).toEqual(expected) 31 | type test = Expect> 32 | }) 33 | -------------------------------------------------------------------------------- /src/utils/object-keys/delimiter-keys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DelimiterCase, 3 | delimiterCase, 4 | } from '../word-case/delimiter-case.js' 5 | import { transformKeys } from './transform-keys.js' 6 | 7 | /** 8 | * Shallowly transforms the keys of a Record to a custom delimiter case. 9 | * T: the type of the Record to transform. 10 | * D: the delimiter to use. 11 | */ 12 | export type DelimiterKeys = T extends [] 13 | ? T 14 | : { [K in keyof T as DelimiterCase, D>]: T[K] } 15 | /** 16 | * A strongly typed function that shallowly transforms the keys of an object to a custom delimiter case. The transformation is done both at runtime and type level. 17 | * @param obj the object to transform. 18 | * @param delimiter the delimiter to use. 19 | * @returns the transformed object. 20 | * @example delimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } 21 | */ 22 | export function delimiterKeys( 23 | obj: T, 24 | delimiter: D 25 | ): DelimiterKeys { 26 | return transformKeys(obj, (str) => delimiterCase(str, delimiter)) as never 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/object-keys/kebab-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type KebabKeys, kebabKeys } from './kebab-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | KebabKeys<{ 7 | someValue: { deepNested: true } 8 | otherValue: true 9 | }>, 10 | { 'some-value': { deepNested: true }; 'other-value': true } 11 | > 12 | > 13 | } 14 | 15 | test('kebabKeys', () => { 16 | const expected = { 17 | some: { deepNested: { value: true } }, 18 | 'other-value': true, 19 | } 20 | const result = kebabKeys({ 21 | some: { deepNested: { value: true } }, 22 | otherValue: true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/kebab-keys.ts: -------------------------------------------------------------------------------- 1 | import { type KebabCase, kebabCase } from '../word-case/kebab-case.js' 2 | import { transformKeys } from './transform-keys.js' 3 | 4 | /** 5 | * Shallowly transforms the keys of a Record to kebab-case. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type KebabKeys = T extends [] 9 | ? T 10 | : { 11 | [K in keyof T as KebabCase>]: T[K] 12 | } 13 | /** 14 | * A strongly typed function that shallowly transforms the keys of an object to kebab-case. The transformation is done both at runtime and type level. 15 | * @param obj the object to transform. 16 | * @returns the transformed object. 17 | * @example kebabKeys({ fooBar: { fizzBuzz: true } }) // { 'foo-bar': { fizzBuzz: true } } 18 | */ 19 | export function kebabKeys(obj: T): KebabKeys { 20 | return transformKeys(obj, kebabCase) as never 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/object-keys/pascal-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type PascalKeys, pascalKeys } from './pascal-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | PascalKeys<{ 7 | someValue: { deepNested: true } 8 | otherValue: true 9 | }>, 10 | { SomeValue: { deepNested: true }; OtherValue: true } 11 | > 12 | > 13 | } 14 | 15 | test('pascalKeys', () => { 16 | const expected = { 17 | Some: { deepNested: { value: true } }, 18 | OtherValue: true, 19 | } 20 | const result = pascalKeys({ 21 | some: { deepNested: { value: true } }, 22 | otherValue: true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/pascal-keys.ts: -------------------------------------------------------------------------------- 1 | import { type PascalCase, pascalCase } from '../word-case/pascal-case.js' 2 | import { transformKeys } from './transform-keys.js' 3 | 4 | /** 5 | * Shallowly transforms the keys of a Record to PascalCase. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type PascalKeys = T extends [] 9 | ? T 10 | : { [K in keyof T as PascalCase>]: T[K] } 11 | /** 12 | * A strongly typed function that shallowly transforms the keys of an object to pascal case. The transformation is done both at runtime and type level. 13 | * @param obj the object to transform. 14 | * @returns the transformed object. 15 | * @example pascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { 'fizz-buzz': true } } 16 | */ 17 | export function pascalKeys(obj: T): PascalKeys { 18 | return transformKeys(obj, pascalCase) as never 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/object-keys/replace-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type ReplaceKeys, replaceKeys } from './replace-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | ReplaceKeys< 7 | { 8 | 'some-value': { 'deep-nested': true } 9 | 'other-value': true 10 | }, 11 | 'some-', 12 | '' 13 | >, 14 | { value: { 'deep-nested': true }; 'other-value': true } 15 | > 16 | > 17 | type testWithUnion = Expect< 18 | Equal< 19 | ReplaceKeys, 'oo', 'izz'>, 20 | Record<'fizz' | 'bar', string> 21 | > 22 | > 23 | type test2 = Expect< 24 | Equal< 25 | ReplaceKeys, RegExp, '-'>, 26 | Record 27 | > 28 | > 29 | type test3 = Expect< 30 | Equal, ' ', '-'>, Record> 31 | > 32 | type test4 = Expect< 33 | Equal< 34 | ReplaceKeys, string>, ' ', '-'>, 35 | Record 36 | > 37 | > 38 | type test5 = Expect< 39 | Equal< 40 | ReplaceKeys, string, '-'>, 41 | Record 42 | > 43 | > 44 | type test6 = Expect< 45 | Equal< 46 | ReplaceKeys, ' ', string>, 47 | Record 48 | > 49 | > 50 | } 51 | 52 | test('replaceKeys', () => { 53 | const expected = { 54 | some: { deepNested: { value: true } }, 55 | value: true, 56 | } 57 | const result = replaceKeys( 58 | { 59 | some: { deepNested: { value: true } }, 60 | other_value: true, 61 | }, 62 | 'other_', 63 | '' 64 | ) 65 | expect(result).toEqual(expected) 66 | type test = Expect> 67 | }) 68 | -------------------------------------------------------------------------------- /src/utils/object-keys/replace-keys.ts: -------------------------------------------------------------------------------- 1 | import { type Replace, replace } from '../../native/replace.js' 2 | import { transformKeys } from './transform-keys.js' 3 | 4 | /** 5 | * Shallowly transforms the keys of a Record with `replace`. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type ReplaceKeys< 9 | T, 10 | lookup extends string | RegExp, 11 | replacement extends string = '', 12 | > = T extends [] 13 | ? T 14 | : { 15 | [K in keyof T as Replace, lookup, replacement>]: T[K] 16 | } 17 | /** 18 | * A strongly typed function that shallowly transforms the keys of an object by running the `replace` method in every key. The transformation is done both at runtime and type level. 19 | * @param obj the object to transform. 20 | * @param lookup the lookup string to be replaced. 21 | * @param replacement the replacement string. 22 | * @returns the transformed object. 23 | * @example replaceKeys({ 'foo-bar': { 'fizz-buzz': true } }, 'f', 'b') // { booBar: { 'fizz-buz': true } } 24 | */ 25 | export function replaceKeys< 26 | T, 27 | S extends string | RegExp, 28 | R extends string = '', 29 | >(obj: T, lookup: S, replacement: R = '' as R): ReplaceKeys { 30 | return transformKeys(obj, (s) => replace(s, lookup, replacement)) as never 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/object-keys/snake-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { type SnakeKeys, snakeKeys } from './snake-keys.js' 2 | 3 | namespace TypeTransforms { 4 | type test = Expect< 5 | Equal< 6 | SnakeKeys<{ 7 | 'some-value': { 'deep-nested': true } 8 | 'other-value': true 9 | }>, 10 | { some_value: { 'deep-nested': true }; other_value: true } 11 | > 12 | > 13 | } 14 | 15 | test('snakeKeys', () => { 16 | const expected = { 17 | some: { deepNested: { value: true } }, 18 | other_value: true, 19 | } 20 | const result = snakeKeys({ 21 | some: { deepNested: { value: true } }, 22 | otherValue: true, 23 | }) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/object-keys/snake-keys.ts: -------------------------------------------------------------------------------- 1 | import { type SnakeCase, snakeCase } from '../word-case/snake-case.js' 2 | import { transformKeys } from './transform-keys.js' 3 | 4 | /** 5 | * Shallowly transforms the keys of a Record to snake_case. 6 | * T: the type of the Record to transform. 7 | */ 8 | export type SnakeKeys = T extends [] 9 | ? T 10 | : { [K in keyof T as SnakeCase>]: T[K] } 11 | /** 12 | * A strongly typed function that shallowly the keys of an object to snake_case. The transformation is done both at runtime and type level. 13 | * @param obj the object to transform. 14 | * @returns the transformed object. 15 | * @example snakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz-buzz': true } } 16 | */ 17 | export function snakeKeys(obj: T): SnakeKeys { 18 | return transformKeys(obj, snakeCase) as never 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/object-keys/transform-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { transformKeys } from './transform-keys.js' 2 | 3 | describe('transformKeys', () => { 4 | test('should shallowly transform the keys of an object', () => { 5 | const expected = { 6 | SOME: { 'deep-nested': { value: true } }, 7 | 'OTHER-VALUE': true, 8 | } 9 | const result = transformKeys( 10 | { 11 | some: { 'deep-nested': { value: true } }, 12 | 'other-value': true, 13 | }, 14 | (key) => key.toUpperCase() 15 | ) 16 | expect(result).toEqual(expected) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/utils/object-keys/transform-keys.ts: -------------------------------------------------------------------------------- 1 | import { typeOf } from '../../internal/internals.js' 2 | 3 | /** 4 | * This function is used to shallowly transform the keys of an object. 5 | * It will only be transformed at runtime, so it's not type safe. 6 | * @param obj the object to transform. 7 | * @param transform the function to transform the keys from string to string. 8 | * @returns the transformed object. 9 | * @example transformKeys({ 'foo-bar': { 'fizz-buzz': true } }, camelCase) 10 | * // { fooBar: { 'fizz-buzz': true } } 11 | */ 12 | export function transformKeys(obj: T, transform: (s: string) => string): T { 13 | if (typeOf(obj) !== 'object') return obj 14 | 15 | const res = {} as T 16 | for (const key in obj) { 17 | res[transform(key) as keyof T] = obj[key] 18 | } 19 | return res 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/reverse.test.ts: -------------------------------------------------------------------------------- 1 | import { type Reverse, reverse } from './reverse' 2 | 3 | namespace ReverseTests { 4 | type test1 = Expect, 'olleh'>> 5 | type test2 = Expect, '321'>> 6 | type test3 = Expect< 7 | Equal, '!tpircSepyT evol I'> 8 | > 9 | type test4 = Expect, string>> 10 | type test5 = Expect>, Uppercase>> 11 | 12 | // Template strings 13 | type testTS1 = Expect, `${string}cba`>> 14 | type testTS2 = Expect, `zyx${string}cba`>> 15 | type testTS3 = Expect, `zyx${string}`>> 16 | } 17 | 18 | describe('reverse', () => { 19 | test('should reverse a string', () => { 20 | const expected = '!desrever eb ot eraperp ,dlrow lufituaeb olleH' 21 | const data = 'Hello beautiful world, prepare to be reversed!' 22 | const result = reverse(data) 23 | expect(result).toEqual(expected) 24 | type test = Expect> 25 | }) 26 | 27 | test('should reverse a long string', () => { 28 | const expected = 29 | 'murobal tse di mina tillom tnuresed aiciffo iuq apluc ni tnus ,tnediorp non tatadipuc taceacco tnis ruetpecxE .rutairap allun taiguf ue erolod mullic esse tilev etatpulov ni tiredneherper ni rolod eruri etua siuD .tauqesnoc odommoc ae xe piuqila tu isin sirobal ocmallu noitaticrexe durtson siuq ,mainev minim da mine tU .auqila angam erolod te erobal tu tnudidicni ropmet domsuie od des ,tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL' 30 | const data = 31 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum' 32 | const result = reverse(data) 33 | expect(result).toEqual(expected) 34 | type test = Expect> 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/utils/reverse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reverses a string. 3 | * - `T` The string to reverse. 4 | */ 5 | type Reverse< 6 | T extends string, 7 | _acc extends string = '', 8 | > = T extends `${infer Head}${infer Tail}` 9 | ? Reverse 10 | : _acc extends '' 11 | ? T 12 | : `${T}${_acc}` 13 | 14 | /** 15 | * A strongly-typed function to reverse a string. 16 | * @param str the string to reverse. 17 | * @returns the reversed string in both type level and runtime. 18 | * @example reverse('hello world') // 'dlrow olleh' 19 | */ 20 | function reverse(str: T) { 21 | return str.split('').reverse().join('') as Reverse 22 | } 23 | 24 | export type { Reverse } 25 | export { reverse } 26 | -------------------------------------------------------------------------------- /src/utils/truncate.test.ts: -------------------------------------------------------------------------------- 1 | import { type Truncate, truncate } from './truncate.js' 2 | 3 | namespace TruncateTests { 4 | type test1 = Expect, 'Hello,...'>> 5 | type test2 = Expect, 'Hello, world'>> 6 | type test3 = Expect, '...'>> 7 | type test4 = Expect, 'Hell[...]'>> 8 | type test5 = Expect, '...'>> 9 | type test6 = Expect, '[...]'>> 10 | type test7 = Expect, string>> 11 | type test8 = Expect, 0, '[...]'>, string>> 12 | type test9 = Expect, string>> 13 | type test10 = Expect, string>> 14 | } 15 | 16 | describe('truncate', () => { 17 | test('truncate small sentence does nothing', () => { 18 | const expected = 'Hello' as const 19 | const result = truncate('Hello', 9) 20 | expect(result).toEqual(expected) 21 | type test = Expect> 22 | }) 23 | 24 | test('truncate big sentence truncate', () => { 25 | const expected = 'Hello ...' as const 26 | const result = truncate('Hello world', 9) 27 | expect(result).toEqual(expected) 28 | type test = Expect> 29 | }) 30 | 31 | test('truncate with negative integer does truncate', () => { 32 | const expected = '...' as const 33 | const result = truncate('Hello world', -1) 34 | expect(result).toEqual(expected) 35 | type test = Expect> 36 | }) 37 | 38 | test('truncate big sentence with specified omission', () => { 39 | const expected = 'Hello[...]' as const 40 | const result = truncate('Hello world', 10, '[...]') 41 | expect(result).toEqual(expected) 42 | type test = Expect> 43 | }) 44 | 45 | test('truncate small sentence with specified omission', () => { 46 | const expected = 'Hello' as const 47 | const result = truncate('Hello', 10, '[...]') 48 | expect(result).toEqual(expected) 49 | type test = Expect> 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/utils/truncate.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | All, 3 | IsNumberLiteral, 4 | IsStringLiteral, 5 | } from '../internal/literals.js' 6 | import type { Math } from '../internal/math.js' 7 | import { type Join, join } from '../native/join.js' 8 | import type { Length } from '../native/length.js' 9 | import type { Slice } from '../native/slice.js' 10 | 11 | // STRING FUNCTIONS 12 | 13 | /** 14 | * Truncate a string if it's longer than the given maximum length. 15 | * The last characters of the truncated string are replaced with the omission string which defaults to "...". 16 | */ 17 | export type Truncate< 18 | T extends string, 19 | Size extends number, 20 | Omission extends string = '...', 21 | > = All<[IsStringLiteral, IsNumberLiteral]> extends true 22 | ? Math.IsNegative extends true 23 | ? Omission 24 | : Math.Subtract, Size> extends 0 25 | ? T 26 | : Join<[Slice>>, Omission]> 27 | : string 28 | 29 | /** 30 | * A strongly typed function to truncate a string if it's longer than the given maximum string length. 31 | * The last characters of the truncated string are replaced with the omission string which defaults to "...". 32 | * @param sentence the sentence to extract the words from. 33 | * @param length the maximum length of the string. 34 | * @param omission the string to append to the end of the truncated string. 35 | * @returns the truncated string 36 | * @example truncate('Hello, World', 8) // 'Hello...' 37 | */ 38 | export function truncate< 39 | T extends string, 40 | S extends number, 41 | P extends string = '...', 42 | >(sentence: T, length: S, omission = '...' as P) { 43 | if (length < 0) return omission as Truncate 44 | if (sentence.length <= length) return sentence as Truncate 45 | return join([ 46 | sentence.slice(0, length - omission.length), 47 | omission, 48 | ]) as Truncate 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/word-case/camel-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | WEIRD_TEXT, 4 | type WeirdTextUnion, 5 | } from '../../internal/fixtures.js' 6 | import { type CamelCase, camelCase } from './camel-case.js' 7 | 8 | namespace TypeTransforms { 9 | type test = Expect< 10 | Equal< 11 | CamelCase, 12 | | 'someWeirdCased$*String1986FooBarWForWumbo' 13 | | 'wheresTheLeakMaam' 14 | | 'dontDistributeUnions' 15 | > 16 | > 17 | } 18 | 19 | describe('camelCase', () => { 20 | test('casing functions', () => { 21 | const expected = 'someWeirdCased$*String1986FooBarWForWumbo' as const 22 | const result = camelCase(WEIRD_TEXT) 23 | expect(result).toEqual(expected) 24 | type test = Expect> 25 | }) 26 | test('with various separators', () => { 27 | const result = camelCase(SEPARATORS_TEXT) 28 | const expected = 'oneTwoThreeFourFiveSixSevenEightNineTen' 29 | expect(result).toEqual(expected) 30 | type test = Expect> 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/utils/word-case/camel-case.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type RemoveApostrophe, 3 | removeApostrophe, 4 | } from '../characters/apostrophe.js' 5 | import { type PascalCase, pascalCase } from './pascal-case.js' 6 | import { uncapitalize } from './uncapitalize.js' 7 | 8 | /** 9 | * Transforms a string to camelCase. 10 | */ 11 | export type CamelCase = Uncapitalize< 12 | PascalCase> 13 | > 14 | 15 | /** 16 | * A strongly typed version of `camelCase` that works in both runtime and type level. 17 | * @param str the string to convert to camel case. 18 | * @returns the camel cased string. 19 | * @example camelCase('hello world') // 'helloWorld' 20 | */ 21 | export function camelCase(str: T): CamelCase { 22 | return uncapitalize(pascalCase(removeApostrophe(str))) 23 | } 24 | 25 | /** 26 | * @deprecated 27 | * Use `camelCase` instead. 28 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 29 | */ 30 | export const toCamelCase = camelCase 31 | -------------------------------------------------------------------------------- /src/utils/word-case/capitalize.test.ts: -------------------------------------------------------------------------------- 1 | import { WEIRD_TEXT } from '../../internal/fixtures.js' 2 | import { capitalize } from './capitalize.js' 3 | 4 | describe('capitalize', () => { 5 | test('it does nothing with a string that has no char at the beginning', () => { 6 | const expected = WEIRD_TEXT 7 | const result = capitalize(WEIRD_TEXT) 8 | expect(result).toEqual(expected) 9 | type test = Expect> 10 | }) 11 | 12 | test('it capitalizes the first char of a string', () => { 13 | const expected = 'SomeWeird-casedString' as const 14 | const result = capitalize('someWeird-casedString') 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/utils/word-case/capitalize.ts: -------------------------------------------------------------------------------- 1 | import { charAt } from '../../native/char-at.js' 2 | import { join } from '../../native/join.js' 3 | import { slice } from '../../native/slice.js' 4 | import { toUpperCase } from '../../native/to-upper-case.js' 5 | 6 | /** 7 | * Capitalizes the first letter of a string. This is a runtime counterpart of `Capitalize` from `src/types.d.ts`. 8 | * @param str the string to capitalize. 9 | * @returns the capitalized string. 10 | * @example capitalize('hello world') // 'Hello world' 11 | */ 12 | export function capitalize(str: T) { 13 | return join([ 14 | toUpperCase(charAt(str, 0) ?? ''), 15 | slice(str, 1), 16 | ]) as Capitalize 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/word-case/constant-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | type WeirdTextUnion, 4 | } from '../../internal/fixtures.js' 5 | import { type ConstantCase, constantCase } from './constant-case.js' 6 | 7 | namespace TypeTransforms { 8 | type test = Expect< 9 | Equal< 10 | ConstantCase, 11 | | 'SOME_WEIRD_CASED_$*_STRING_1986_FOO_BAR_W_FOR_WUMBO' 12 | | 'WHERES_THE_LEAK_MAAM' 13 | | 'DONT_DISTRIBUTE_UNIONS' 14 | > 15 | > 16 | } 17 | 18 | describe('constantCase', () => { 19 | test('casing functions', () => { 20 | const expected = 21 | 'SOME_WEIRD_CASED_$*_STRING_1986_FOO_BAR_W_FOR_WUMBO' as const 22 | const result = constantCase( 23 | ' someWeird-cased$*String1986Foo Bar W_FOR_WUMBO' 24 | ) 25 | expect(result).toEqual(expected) 26 | type test = Expect> 27 | }) 28 | 29 | test('with various separators', () => { 30 | const result = constantCase(SEPARATORS_TEXT) 31 | const expected = 'ONE_TWO_THREE_FOUR_FIVE_SIX_SEVEN_EIGHT_NINE_TEN' 32 | expect(result).toEqual(expected) 33 | type test = Expect> 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/utils/word-case/constant-case.ts: -------------------------------------------------------------------------------- 1 | import { toUpperCase } from '../../native/to-upper-case.js' 2 | import { 3 | type RemoveApostrophe, 4 | removeApostrophe, 5 | } from '../characters/apostrophe.js' 6 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 7 | 8 | /** 9 | * Transforms a string to CONSTANT_CASE. 10 | */ 11 | export type ConstantCase = Uppercase< 12 | DelimiterCase, '_'> 13 | > 14 | /** 15 | * A strongly typed version of `constantCase` that works in both runtime and type level. 16 | * @param str the string to convert to constant case. 17 | * @returns the constant cased string. 18 | * @example constantCase('hello world') // 'HELLO_WORLD' 19 | */ 20 | export function constantCase(str: T): ConstantCase { 21 | return toUpperCase(delimiterCase(removeApostrophe(str), '_')) 22 | } 23 | 24 | /** 25 | * @deprecated 26 | * Use `constantCase` instead. 27 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 28 | */ 29 | export const toConstantCase = constantCase 30 | -------------------------------------------------------------------------------- /src/utils/word-case/delimiter-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | WEIRD_TEXT, 4 | type WeirdTextUnion, 5 | } from '../../internal/fixtures.js' 6 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 7 | 8 | namespace TypeTransforms { 9 | type test = Expect< 10 | Equal< 11 | DelimiterCase, 12 | | 'some%Weird%cased%$*%String%1986%Foo%Bar%W%FOR%WUMBO' 13 | | 'wheres%the%leak%maam' 14 | | 'dont%distribute%unions' 15 | > 16 | > 17 | } 18 | 19 | describe('delimiterCase', () => { 20 | test('casing functions', () => { 21 | const expected = 22 | 'some@Weird@cased@$*@String@1986@Foo@Bar@W@FOR@WUMBO' as const 23 | const result = delimiterCase(WEIRD_TEXT, '@') 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | test('with various separators', () => { 28 | const result = delimiterCase(SEPARATORS_TEXT, '.') 29 | const expected = 'one.two.three.four.five.six.seven.eight.nine.ten' 30 | expect(result).toEqual(expected) 31 | type test = Expect> 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/utils/word-case/delimiter-case.ts: -------------------------------------------------------------------------------- 1 | import { type Join, join } from '../../native/join.js' 2 | import { 3 | type RemoveApostrophe, 4 | removeApostrophe, 5 | } from '../characters/apostrophe.js' 6 | import { type Words, words } from '../words.js' 7 | 8 | /** 9 | * Transforms a string with the specified separator (delimiter). 10 | */ 11 | export type DelimiterCase = Join< 12 | Words>, 13 | D 14 | > 15 | /** 16 | * A function that transforms a string by splitting it into words and joining them with the specified delimiter. 17 | * @param str the string to transform. 18 | * @param delimiter the delimiter to use. 19 | * @returns the transformed string. 20 | * @example delimiterCase('hello world', '.') // 'hello.world' 21 | */ 22 | export function delimiterCase( 23 | str: T, 24 | delimiter: D 25 | ): DelimiterCase { 26 | return join(words(removeApostrophe(str)), delimiter) 27 | } 28 | 29 | /** 30 | * @deprecated 31 | * Use `delimiterCase` instead. 32 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 33 | */ 34 | export const toDelimiterCase = delimiterCase 35 | -------------------------------------------------------------------------------- /src/utils/word-case/kebab-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | WEIRD_TEXT, 4 | type WeirdTextUnion, 5 | } from '../../internal/fixtures.js' 6 | import { type KebabCase, kebabCase } from './kebab-case.js' 7 | 8 | namespace TypeTransforms { 9 | type test = Expect< 10 | Equal< 11 | KebabCase, 12 | | 'some-weird-cased-$*-string-1986-foo-bar-w-for-wumbo' 13 | | 'wheres-the-leak-maam' 14 | | 'dont-distribute-unions' 15 | > 16 | > 17 | } 18 | 19 | describe('kebabCase', () => { 20 | test('casing functions', () => { 21 | const expected = 22 | 'some-weird-cased-$*-string-1986-foo-bar-w-for-wumbo' as const 23 | const result = kebabCase(WEIRD_TEXT) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | test('with various separators', () => { 28 | const result = kebabCase(SEPARATORS_TEXT) 29 | const expected = 'one-two-three-four-five-six-seven-eight-nine-ten' 30 | expect(result).toEqual(expected) 31 | type test = Expect> 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/utils/word-case/kebab-case.ts: -------------------------------------------------------------------------------- 1 | import { toLowerCase } from '../../native/to-lower-case.js' 2 | import { 3 | type RemoveApostrophe, 4 | removeApostrophe, 5 | } from '../characters/apostrophe.js' 6 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 7 | 8 | /** 9 | * Transforms a string to kebab-case. 10 | */ 11 | export type KebabCase = Lowercase< 12 | DelimiterCase, '-'> 13 | > 14 | /** 15 | * A strongly typed version of `kebabCase` that works in both runtime and type level. 16 | * @param str the string to convert to kebab case. 17 | * @returns the kebab cased string. 18 | * @example kebabCase('hello world') // 'hello-world' 19 | */ 20 | export function kebabCase(str: T): KebabCase { 21 | return toLowerCase(delimiterCase(removeApostrophe(str), '-')) 22 | } 23 | 24 | /** 25 | * @deprecated 26 | * Use `kebabCase` instead. 27 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 28 | */ 29 | export const toKebabCase = kebabCase 30 | -------------------------------------------------------------------------------- /src/utils/word-case/lower-case.test.ts: -------------------------------------------------------------------------------- 1 | import { SEPARATORS_TEXT, WEIRD_TEXT } from '../../internal/fixtures.js' 2 | import { lowerCase } from './lower-case.js' 3 | 4 | describe('lowerCase', () => { 5 | test('casing functions', () => { 6 | const expected = 7 | 'some weird cased $* string 1986 foo bar w for wumbo' as const 8 | const result = lowerCase(WEIRD_TEXT) 9 | expect(result).toEqual(expected) 10 | type test = Expect> 11 | }) 12 | 13 | test('lowerCase', () => { 14 | const result = lowerCase(SEPARATORS_TEXT) 15 | const expected = 'one two three four five six seven eight nine ten' 16 | expect(result).toEqual(expected) 17 | type test = Expect> 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/utils/word-case/lower-case.ts: -------------------------------------------------------------------------------- 1 | import { toLowerCase } from '../../native/to-lower-case.js' 2 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 3 | 4 | /** 5 | * A strongly-typed version of `lowerCase` that works in both runtime and type level. 6 | * @param str the string to convert to lower case. 7 | * @returns the lowercased string. 8 | * @example lowerCase('HELLO-WORLD') // 'hello world' 9 | */ 10 | export function lowerCase( 11 | str: T 12 | ): Lowercase> { 13 | return toLowerCase(delimiterCase(str, ' ')) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/word-case/pascal-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | WEIRD_TEXT, 4 | type WeirdTextUnion, 5 | } from '../../internal/fixtures.js' 6 | import { type PascalCase, pascalCase } from './pascal-case.js' 7 | 8 | namespace TypeTransforms { 9 | type test = Expect< 10 | Equal< 11 | PascalCase, 12 | | 'SomeWeirdCased$*String1986FooBarWForWumbo' 13 | | 'WheresTheLeakMaam' 14 | | 'DontDistributeUnions' 15 | > 16 | > 17 | } 18 | 19 | describe('pascalCase', () => { 20 | test('casing functions', () => { 21 | const expected = 'SomeWeirdCased$*String1986FooBarWForWumbo' as const 22 | const result = pascalCase(WEIRD_TEXT) 23 | expect(result).toEqual(expected) 24 | type test = Expect> 25 | }) 26 | test('with various separators', () => { 27 | const result = pascalCase(SEPARATORS_TEXT) 28 | const expected = 'OneTwoThreeFourFiveSixSevenEightNineTen' 29 | expect(result).toEqual(expected) 30 | type test = Expect> 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/utils/word-case/pascal-case.ts: -------------------------------------------------------------------------------- 1 | import { type PascalCaseAll, pascalCaseAll } from '../../internal/internals.js' 2 | import { type Join, join } from '../../native/join.js' 3 | import { 4 | type RemoveApostrophe, 5 | removeApostrophe, 6 | } from '../characters/apostrophe.js' 7 | import { type Words, words } from '../words.js' 8 | 9 | /** 10 | * Transforms a string to PascalCase. 11 | */ 12 | export type PascalCase = Join< 13 | PascalCaseAll>> 14 | > 15 | /** 16 | * A strongly typed version of `pascalCase` that works in both runtime and type level. 17 | * @param str the string to convert to pascal case. 18 | * @returns the pascal cased string. 19 | * @example pascalCase('hello world') // 'HelloWorld' 20 | */ 21 | export function pascalCase(str: T): PascalCase { 22 | return join(pascalCaseAll(words(removeApostrophe(str)))) 23 | } 24 | 25 | /** 26 | * @deprecated 27 | * Use `pascalCase` instead. 28 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 29 | */ 30 | export const toPascalCase = pascalCase 31 | -------------------------------------------------------------------------------- /src/utils/word-case/snake-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | WEIRD_TEXT, 4 | type WeirdTextUnion, 5 | } from '../../internal/fixtures.js' 6 | import { type SnakeCase, snakeCase } from './snake-case.js' 7 | 8 | namespace TypeTransforms { 9 | type test = Expect< 10 | Equal< 11 | SnakeCase, 12 | | 'some_weird_cased_$*_string_1986_foo_bar_w_for_wumbo' 13 | | 'wheres_the_leak_maam' 14 | | 'dont_distribute_unions' 15 | > 16 | > 17 | } 18 | 19 | describe('snakeCase', () => { 20 | test('casing functions', () => { 21 | const expected = 22 | 'some_weird_cased_$*_string_1986_foo_bar_w_for_wumbo' as const 23 | const result = snakeCase(WEIRD_TEXT) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | test('with various separators', () => { 28 | const result = snakeCase(SEPARATORS_TEXT) 29 | const expected = 'one_two_three_four_five_six_seven_eight_nine_ten' 30 | expect(result).toEqual(expected) 31 | type test = Expect> 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/utils/word-case/snake-case.ts: -------------------------------------------------------------------------------- 1 | import { toLowerCase } from '../../native/to-lower-case.js' 2 | import { 3 | type RemoveApostrophe, 4 | removeApostrophe, 5 | } from '../characters/apostrophe.js' 6 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 7 | 8 | /** 9 | * Transforms a string to snake_case. 10 | */ 11 | export type SnakeCase = Lowercase< 12 | DelimiterCase, '_'> 13 | > 14 | /** 15 | * A strongly typed version of `snakeCase` that works in both runtime and type level. 16 | * @param str the string to convert to snake case. 17 | * @returns the snake cased string. 18 | * @example snakeCase('hello world') // 'hello_world' 19 | */ 20 | export function snakeCase(str: T): SnakeCase { 21 | return toLowerCase(delimiterCase(removeApostrophe(str), '_')) 22 | } 23 | 24 | /** 25 | * @deprecated 26 | * Use `snakeCase` instead. 27 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 28 | */ 29 | export const toSnakeCase = snakeCase 30 | -------------------------------------------------------------------------------- /src/utils/word-case/title-case.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SEPARATORS_TEXT, 3 | WEIRD_TEXT, 4 | type WeirdTextUnion, 5 | } from '../../internal/fixtures.js' 6 | import { type TitleCase, titleCase } from './title-case.js' 7 | 8 | namespace TypeTransforms { 9 | type test = Expect< 10 | Equal< 11 | TitleCase, 12 | | 'Some Weird Cased $* String 1986 Foo Bar W For Wumbo' 13 | | 'Wheres The Leak Maam' 14 | | 'Dont Distribute Unions' 15 | > 16 | > 17 | } 18 | 19 | describe('titleCase', () => { 20 | test('casing functions', () => { 21 | const expected = 22 | 'Some Weird Cased $* String 1986 Foo Bar W For Wumbo' as const 23 | const result = titleCase(WEIRD_TEXT) 24 | expect(result).toEqual(expected) 25 | type test = Expect> 26 | }) 27 | test('with various separators', () => { 28 | const result = titleCase(SEPARATORS_TEXT) 29 | const expected = 'One Two Three Four Five Six Seven Eight Nine Ten' 30 | expect(result).toEqual(expected) 31 | type test = Expect> 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/utils/word-case/title-case.ts: -------------------------------------------------------------------------------- 1 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 2 | import { type PascalCase, pascalCase } from './pascal-case.js' 3 | 4 | /** 5 | * Transforms a string to "Title Case". 6 | */ 7 | export type TitleCase = DelimiterCase, ' '> 8 | /** 9 | * A strongly typed version of `titleCase` that works in both runtime and type level. 10 | * @param str the string to convert to title case. 11 | * @returns the title cased string. 12 | * @example titleCase('hello world') // 'Hello World' 13 | */ 14 | export function titleCase(str: T): TitleCase { 15 | return delimiterCase(pascalCase(str), ' ') 16 | } 17 | 18 | /** 19 | * @deprecated 20 | * Use `titleCase` instead. 21 | * Read more about the deprecation [here](https://github.com/gustavoguichard/string-ts/issues/44). 22 | */ 23 | export const toTitleCase = titleCase 24 | -------------------------------------------------------------------------------- /src/utils/word-case/uncapitalize.test.ts: -------------------------------------------------------------------------------- 1 | import { WEIRD_TEXT } from '../../internal/fixtures.js' 2 | import { uncapitalize } from './uncapitalize.js' 3 | 4 | describe('uncapitalize', () => { 5 | test('it does nothing with a string that has no char at the beginning', () => { 6 | const expected = WEIRD_TEXT 7 | const result = uncapitalize(WEIRD_TEXT) 8 | expect(result).toEqual(expected) 9 | type test = Expect> 10 | }) 11 | 12 | test('it uncapitalizes the first char of a string', () => { 13 | const expected = 'someWeird-casedString' as const 14 | const result = uncapitalize('SomeWeird-casedString') 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/utils/word-case/uncapitalize.ts: -------------------------------------------------------------------------------- 1 | import { charAt } from '../../native/char-at.js' 2 | import { join } from '../../native/join.js' 3 | import { slice } from '../../native/slice.js' 4 | import { toLowerCase } from '../../native/to-lower-case.js' 5 | 6 | /** 7 | * Uncapitalizes the first letter of a string. This is a runtime counterpart of `Uncapitalize` from `src/types.d.ts`. 8 | * @param str the string to uncapitalize. 9 | * @returns the uncapitalized string. 10 | * @example uncapitalize('Hello world') // 'hello world' 11 | */ 12 | export function uncapitalize(str: T) { 13 | return join([ 14 | toLowerCase(charAt(str, 0) ?? ''), 15 | slice(str, 1), 16 | ]) as Uncapitalize 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/word-case/upper-case.test.ts: -------------------------------------------------------------------------------- 1 | import { SEPARATORS_TEXT, WEIRD_TEXT } from '../../internal/fixtures.js' 2 | import { upperCase } from './upper-case.js' 3 | 4 | describe('upperCase', () => { 5 | test('casing functions', () => { 6 | const expected = 7 | 'SOME WEIRD CASED $* STRING 1986 FOO BAR W FOR WUMBO' as const 8 | const result = upperCase(WEIRD_TEXT) 9 | expect(result).toEqual(expected) 10 | type test = Expect> 11 | }) 12 | test('upperCase', () => { 13 | const result = upperCase(SEPARATORS_TEXT) 14 | const expected = 'ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE TEN' 15 | expect(result).toEqual(expected) 16 | type test = Expect> 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/utils/word-case/upper-case.ts: -------------------------------------------------------------------------------- 1 | import { toUpperCase } from '../../native/to-upper-case.js' 2 | import { type DelimiterCase, delimiterCase } from './delimiter-case.js' 3 | 4 | /** 5 | * A strongly-typed version of `upperCase` that works in both runtime and type level. 6 | * @param str the string to convert to upper case. 7 | * @returns the uppercased string. 8 | * @example upperCase('hello-world') // 'HELLO WORLD' 9 | */ 10 | export function upperCase( 11 | str: T 12 | ): Uppercase> { 13 | return toUpperCase(delimiterCase(str, ' ')) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/words.test.ts: -------------------------------------------------------------------------------- 1 | import type { Words } from './words.js' 2 | import { words } from './words.js' 3 | 4 | namespace WordsTests { 5 | type test1 = Expect< 6 | Equal< 7 | Words<' someWeird-cased$*String1986Foo Bar obj.items[0]'>, 8 | [ 9 | 'some', 10 | 'Weird', 11 | 'cased', 12 | '$*', 13 | 'String', 14 | '1986', 15 | 'Foo', 16 | 'Bar', 17 | 'obj', 18 | 'items', 19 | '0', 20 | ] 21 | > 22 | > 23 | type test2 = Expect, string[]>> 24 | type test3 = Expect, string[]>> 25 | type test4 = Expect, string[]>> 26 | type test5 = Expect< 27 | Equal, ["Where's", 'the', 'leak', "ma'am"]> 28 | > 29 | } 30 | 31 | type Mutable = { 32 | -readonly [Key in keyof Type]: Type[Key] 33 | } 34 | 35 | describe('words', () => { 36 | test('it splits words at separators', () => { 37 | const expected = [ 38 | 'one', 39 | 'two', 40 | 'three', 41 | 'four', 42 | 'five', 43 | 'six', 44 | 'seven', 45 | 'eight', 46 | 'nine', 47 | 'ten', 48 | ] as const 49 | const result = words( 50 | '[one] two-three/four.five(six){seven}|eight_nine\\ten' 51 | ) 52 | expect(result).toEqual(expected) 53 | type test = Expect>> 54 | }) 55 | 56 | test('it splits words at digits', () => { 57 | const expected = ['2', 'Weird', 'Cased', '1986', 'Foo'] as const 58 | const result = words('2WeirdCased1986Foo') 59 | expect(result).toEqual(expected) 60 | type test = Expect>> 61 | }) 62 | 63 | test('it splits words at special chars', () => { 64 | const expected = ['$', '2', 'Weird', 'Cased', '@@', 'Foo'] as const 65 | const result = words('$2WeirdCased@@Foo') 66 | expect(result).toEqual(expected) 67 | type test = Expect>> 68 | }) 69 | 70 | test('it splits words at casing', () => { 71 | const expected = ['some', 'Weird', 'Cased', 'STRING', 'Foo'] as const 72 | const result = words('someWeirdCasedSTRINGFoo') 73 | expect(result).toEqual(expected) 74 | type test = Expect>> 75 | }) 76 | 77 | test('it preserves apostrophes', () => { 78 | const expected = ["Where's", 'the', 'leak', "ma'am"] as const 79 | const result = words("Where's the leak ma'am") 80 | expect(result).toEqual(expected) 81 | type test = Expect>> 82 | }) 83 | 84 | test('it combines all of the rules above and trims the word', () => { 85 | const expected = [ 86 | 'some', 87 | 'Weird', 88 | 'cased', 89 | '$*', 90 | 'String', 91 | '1986', 92 | 'Foo', 93 | 'Bar', 94 | ] as const 95 | const result = words(' someWeird-cased$*String1986Foo Bar ') 96 | expect(result).toEqual(expected) 97 | type test = Expect>> 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/utils/words.ts: -------------------------------------------------------------------------------- 1 | import type { DropSuffix, Reject } from '../internal/internals.js' 2 | import type { IsStringLiteral } from '../internal/literals.js' 3 | import type { IsLower, IsUpper } from './characters/letters.js' 4 | import type { IsDigit } from './characters/numbers.js' 5 | import type { IsSeparator } from './characters/separators.js' 6 | import { SEPARATOR_REGEX } from './characters/separators.js' 7 | import type { IsSpecial } from './characters/special.js' 8 | 9 | /** 10 | * Splits a string into words. 11 | * sentence: The current string to split. 12 | * word: The current word. 13 | * prev: The previous character. 14 | */ 15 | export type Words< 16 | sentence extends string, 17 | word extends string = '', 18 | prev extends string = '', 19 | > = IsStringLiteral extends true 20 | ? sentence extends `${infer curr}${infer rest}` 21 | ? IsSeparator extends true 22 | ? // Step 1: Remove separators 23 | Reject<[word, ...Words], ''> 24 | : prev extends '' 25 | ? // Start of sentence, start a new word 26 | Reject, ''> 27 | : [false, true] extends [IsDigit, IsDigit] 28 | ? // Step 2: From non-digit to digit 29 | [word, ...Words] 30 | : [true, false] extends [IsDigit, IsDigit] 31 | ? // Step 3: From digit to non-digit 32 | [word, ...Words] 33 | : [false, true] extends [IsSpecial, IsSpecial] 34 | ? // Step 4: From non-special to special 35 | [word, ...Words] 36 | : [true, false] extends [IsSpecial, IsSpecial] 37 | ? // Step 5: From special to non-special 38 | [word, ...Words] 39 | : [true, true] extends [IsDigit, IsDigit] 40 | ? // If both are digit, continue with the sentence 41 | Reject, ''> 42 | : [true, true] extends [IsLower, IsUpper] 43 | ? // Step 6: From lower to upper 44 | [word, ...Words] 45 | : [true, true] extends [IsUpper, IsLower] 46 | ? // Step 7: From upper to upper and lower 47 | // Remove the last character from the current word and start a new word with it 48 | [ 49 | DropSuffix, 50 | ...Words, 51 | ] 52 | : Reject, ''> // Otherwise continue with the sentence 53 | : // Step 8: Trim the last word 54 | Reject<[word], ''> 55 | : string[] // Avoid spending resources on a wide type 56 | 57 | /** 58 | * A strongly-typed function to extract the words from a sentence. 59 | * @param sentence the sentence to extract the words from. 60 | * @returns an array of words in both type level and runtime. 61 | * @example words('helloWorld') // ['hello', 'World'] 62 | */ 63 | export function words(sentence: T): Words { 64 | return sentence 65 | .replace(SEPARATOR_REGEX, ' ') // Step 1: Remove separators 66 | .replace(/([a-zA-Z])([0-9])/g, '$1 $2') // Step 2: From non-digit to digit 67 | .replace(/([0-9])([a-zA-Z])/g, '$1 $2') // Step 3: From digit to non-digit 68 | .replace(/([a-zA-Z0-9_\-./])([^a-zA-Z0-9_\-./'])/g, '$1 $2') // Step 4: From non-special to special 69 | .replace(/([^a-zA-Z0-9_\-./'])([a-zA-Z0-9_\-./])/g, '$1 $2') // Step 5: From special to non-special 70 | .replace(/([a-z])([A-Z])/g, '$1 $2') // Step 6: From lower to upper 71 | .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // Step 7: From upper to upper and lower 72 | .trim() // Step 8: Trim the last word 73 | .split(/\s+/g) as Words 74 | } 75 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "include": ["dist"], 4 | "compilerOptions": { 5 | "strict": true, 6 | "noEmit": true, 7 | "skipDefaultLibCheck": false, 8 | "skipLibCheck": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "module": "commonjs", 8 | "outDir": "./tsc/", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "target": "ES2021", 12 | "types": ["vitest/globals", "node"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig(() => ({ 5 | test: { 6 | globals: true, 7 | maxConcurrency: 1, 8 | minThreads: 0, 9 | maxThreads: 1, 10 | exclude: ['tsc', 'node_modules'], 11 | }, 12 | })) 13 | --------------------------------------------------------------------------------