diff --git a/README.md b/README.md index 90c679e..d523478 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,14 @@ npm install string-ts - [Runtime counterparts of native type utilities](#runtime-counterparts-of-native-type-utilities) - [capitalize](#capitalize) + - [uncapitalize](#uncapitalize) - [Strongly-typed alternatives to native runtime utilities](#strongly-typed-alternatives-to-native-runtime-utilities) - [chartAt](#charat) - [join](#join) + - [length](#length) - [replace](#replace) - [replaceAll](#replaceall) + - [slice](#slice) - [split](#split) - [toLowerCase](#tolowercase) - [toUpperCase](#touppercase) @@ -146,6 +149,18 @@ const result = capitalize(str) // ^ 'Hello world' ``` +### uncapitalize + +Uncapitalizes the first letter of a string. This is a runtime counterpart of `Uncapitalize` from `src/types.d.ts`. + +```ts +import { uncapitalize } from 'string-ts' + +const str = 'Hello world' +const result = uncapitalize(str) +// ^ 'hello world' +``` + ## Strongly-typed alternatives to native runtime utilities ### charAt @@ -172,6 +187,18 @@ const result = join(str, ' ') // ^ 'hello world' ``` +### length + +This function is a strongly-typed counterpart of `String.prototype.length`. + +```ts +import { length } from 'string-ts' + +const str = 'hello' +const result = length(str) +// ^ 5 +``` + ### replace This function is a strongly-typed counterpart of `String.prototype.replace`. @@ -196,6 +223,24 @@ const result = replaceAll(str, '-', ' ') // ^ 'hello world ' ``` +### slice + +This function is a strongly-typed counterpart of `String.prototype.slice`. + +_Warning: due to TS limitations - for now - we ignore the second argument (endIndex) if the first (startIndex) is negative and we also don't support a negative endIndex._ + +```ts +import { slice } from 'string-ts' + +const str = 'hello-world' +const result = slice(str, 6) +// ^ 'world' +const result2 = slice(str, 1, 5) +// ^ 'ello' +const result3 = slice(str, -5) +// ^ 'world' +``` + ### split This function is a strongly-typed counterpart of `String.prototype.split`. @@ -583,8 +628,10 @@ Uppercase<'hello world'> // 'HELLO WORLD' ```ts St.CharAt<'hello world', 6> // 'w' St.Join<['hello', 'world'], '-'> // 'hello-world' +St.Length<'hello'> // 5 St.Replace<'hello-world', 'l', '1'> // 'he1lo-world' St.ReplaceAll<'hello-world', 'l', '1'> // 'he11o-wor1d' +St.Slice<'hello-world', -5> // 'world' St.Split<'hello-world', '-'> // ['hello', 'world'] St.Trim<' hello world '> // 'hello world' St.TrimEnd<' hello world '> // ' hello world' diff --git a/src/casing.test.ts b/src/casing.test.ts index 891abc9..41db1b3 100644 --- a/src/casing.test.ts +++ b/src/casing.test.ts @@ -70,6 +70,22 @@ describe('capitalize', () => { }) }) +describe('uncapitalize', () => { + test('it does nothing with a string that has no char at the beginning', () => { + const expected = weirdString + const result = subject.uncapitalize(weirdString) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('it uncapitalizes the first char of a string', () => { + const expected = 'someWeird-casedString' as const + const result = subject.uncapitalize('SomeWeird-casedString') + expect(result).toEqual(expected) + type test = Expect> + }) +}) + describe('casing functions', () => { test('toUpperCase', () => { const expected = ' SOMEWEIRD-CASED$*STRING1986FOO BAR W_FOR_WUMBO' as const diff --git a/src/casing.ts b/src/casing.ts index 63f5bac..d21ea98 100644 --- a/src/casing.ts +++ b/src/casing.ts @@ -1,10 +1,21 @@ -import type { PascalCaseAll } from './internals' +import { pascalCaseAll, type PascalCaseAll } from './internals' import type { Join } from './primitives' -import { join } from './primitives' -import type { Is, Words } from './utils' +import { charAt, join, slice } from './primitives' +import type { Words } from './utils' import { words } from './utils' -// CASING UTILITIES +// CASING UTILITIES THAT ALREADY HAVE NATIVE TS TYPES + +/** + * Capitalizes the first letter of a string. This is a runtime counterpart of `Capitalize` from `src/types.d.ts`. + * @param str the string to capitalize. + * @returns the capitalized string. + * @example capitalize('hello world') // 'Hello world' + */ +function capitalize(str: T): Capitalize { + return join([toUpperCase(charAt(str, 0)), slice(str, 1)]) +} + /** * This function is a strongly-typed counterpart of String.prototype.toLowerCase. * @param str the string to make lowercase. @@ -26,15 +37,16 @@ function toUpperCase(str: T) { } /** - * Capitalizes the first letter of a string. This is a runtime counterpart of `Capitalize` from `src/types.d.ts`. - * @param str the string to capitalize. - * @returns the capitalized string. - * @example capitalize('hello world') // 'Hello world' + * Uncapitalizes the first letter of a string. This is a runtime counterpart of `Uncapitalize` from `src/types.d.ts`. + * @param str the string to uncapitalize. + * @returns the uncapitalized string. + * @example uncapitalize('Hello world') // 'hello world' */ -function capitalize(str: T) { - return (toUpperCase(str.charAt(0)) + str.slice(1)) as Capitalize +function uncapitalize(str: T): Uncapitalize { + return join([toLowerCase(charAt(str, 0)), slice(str, 1)]) } +// CASING UTILITIES /** * Transforms a string with the specified separator (delimiter). */ @@ -56,11 +68,7 @@ function toDelimiterCase( /** * Transforms a string to camelCase. */ -type CamelCase = T extends unknown - ? PascalCase extends `${infer first}${infer rest}` - ? `${Lowercase}${rest}` - : T - : never +type CamelCase = Uncapitalize> /** * A strongly typed version of `toCamelCase` that works in both runtime and type level. @@ -68,15 +76,14 @@ type CamelCase = T extends unknown * @returns the camel cased string. * @example toCamelCase('hello world') // 'helloWorld' */ -function toCamelCase(str: T) { - const res = toPascalCase(str) - return (res.slice(0, 1).toLowerCase() + res.slice(1)) as CamelCase +function toCamelCase(str: T): CamelCase { + return uncapitalize(toPascalCase(str)) } /** * Transforms a string to PascalCase. */ -type PascalCase = Join, string[]>>> +type PascalCase = Join>> /** * A strongly typed version of `toPascalCase` that works in both runtime and type level. * @param str the string to convert to pascal case. @@ -84,9 +91,7 @@ type PascalCase = Join, string[]>>> * @example toPascalCase('hello world') // 'HelloWorld' */ function toPascalCase(str: T): PascalCase { - return words(str) - .map((v) => capitalize(toLowerCase(v))) - .join('') as PascalCase + return join(pascalCaseAll(words(str))) } /** @@ -165,4 +170,5 @@ export { toSnakeCase, toTitleCase, toUpperCase, + uncapitalize, } diff --git a/src/index.ts b/src/index.ts index 47d73fa..54da1cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,10 @@ export type { CharAt, Join, + Length, Replace, ReplaceAll, + Slice, Split, TrimStart, TrimEnd, @@ -12,8 +14,10 @@ export type { export { charAt, join, + length, replace, replaceAll, + slice, split, trim, trimStart, @@ -23,7 +27,6 @@ export { // UTILS export type { Digit, - Is, IsDigit, IsLetter, IsLower, @@ -56,6 +59,7 @@ export { toSnakeCase, toTitleCase, toUpperCase, + uncapitalize, } from './casing' // KEY CASING diff --git a/src/internals.ts b/src/internals.ts index 6a020bc..701bcce 100644 --- a/src/internals.ts +++ b/src/internals.ts @@ -1,4 +1,4 @@ -import type { Is } from './utils' +import { capitalize, toLowerCase } from './casing' /** * This is an enhanced version of the typeof operator to check the type of more complex values. @@ -28,6 +28,10 @@ function typeOf(t: unknown) { | 'urlsearchparams' } +function pascalCaseAll(words: T) { + return words.map((v) => capitalize(toLowerCase(v))) as PascalCaseAll +} + /** * Removes all the elements matching the given condition from a tuple. */ @@ -49,12 +53,12 @@ type DropSuffix< /** * PascalCases all the words in a tuple of strings */ -type PascalCaseAll = T extends [infer First, ...infer Rest] - ? [ - Capitalize>>, - ...PascalCaseAll>, - ] +type PascalCaseAll = T extends [ + infer head extends string, + ...infer rest extends string[], +] + ? [Capitalize>, ...PascalCaseAll] : T -export type { PascalCaseAll, Drop, DropSuffix } -export { typeOf } +export type { Drop, DropSuffix, PascalCaseAll } +export { pascalCaseAll, typeOf } diff --git a/src/key-casing.ts b/src/key-casing.ts index 56e2caf..27447b2 100644 --- a/src/key-casing.ts +++ b/src/key-casing.ts @@ -17,6 +17,25 @@ import { import { typeOf } from './internals' import type { Is } from './utils' +/** + * This function is used to shallowly transform the keys of an object. + * It will only be transformed at runtime, so it's not type safe. + * @param obj the object to transform. + * @param transform the function to transform the keys from string to string. + * @returns the transformed object. + * @example transformKeys({ 'foo-bar': { 'fizz-buzz': true } }, toCamelCase) + * // { fooBar: { 'fizz-buzz': true } } + */ +function transformKeys(obj: T, transform: (s: string) => string): T { + if (typeOf(obj) !== 'object') return obj + + const res = {} as T + for (const key in obj) { + res[transform(key) as keyof T] = obj[key] + } + return res +} + /** * This function is used to transform the keys of an object deeply. * It will only be transformed at runtime, so it's not type safe. @@ -40,22 +59,20 @@ function deepTransformKeys(obj: T, transform: (s: string) => string): T { } /** - * This function is used to shallowly transform the keys of an object. - * It will only be transformed at runtime, so it's not type safe. + * Shallowly transforms the keys of an Record to camelCase. + * T: the type of the Record to transform. + */ +type CamelKeys = T extends [] + ? T + : { [K in keyof T as CamelCase>]: T[K] } +/** + * A strongly typed function that shallowly transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. * @param obj the object to transform. - * @param transform the function to transform the keys from string to string. * @returns the transformed object. - * @example transformKeys({ 'foo-bar': { 'fizz-buzz': true } }, toCamelCase) - * // { fooBar: { 'fizz-buzz': true } } + * @example camelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { 'fizz-buz': true } } */ -function transformKeys(obj: T, transform: (s: string) => string): T { - if (typeOf(obj) !== 'object') return obj - - const res = {} as T - for (const key in obj) { - res[transform(key) as keyof T] = obj[key] - } - return res +function camelKeys(obj: T): CamelKeys { + return transformKeys(obj, toCamelCase) as never } /** @@ -69,13 +86,6 @@ type DeepCamelKeys = T extends [any, ...any] : { [K in keyof T as CamelCase>]: DeepCamelKeys } -/** - * Shallowly transforms the keys of an Record to camelCase. - * T: the type of the Record to transform. - */ -type CamelKeys = T extends [] - ? T - : { [K in keyof T as CamelCase>]: T[K] } /** * A strongly typed function that recursively transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. * @param obj the object to transform. @@ -85,64 +95,98 @@ type CamelKeys = T extends [] function deepCamelKeys(obj: T): DeepCamelKeys { return deepTransformKeys(obj, toCamelCase) as never } + /** - * A strongly typed function that shallowly transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. + * Shallowly transforms the keys of an Record to CONSTANT_CASE. + * T: the type of the Record to transform. + */ +type ConstantKeys = T extends [] + ? T + : { [K in keyof T as ConstantCase>]: T[K] } +/** + * 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. * @param obj the object to transform. * @returns the transformed object. - * @example camelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { 'fizz-buz': true } } + * @example constantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { 'fizz-buzz': true } } */ -function camelKeys(obj: T): CamelKeys { - return transformKeys(obj, toCamelCase) as never +function constantKeys(obj: T): ConstantKeys { + return transformKeys(obj, toConstantCase) as never } /** - * Recursively transforms the keys of an Record to PascalCase. + * Recursively transforms the keys of an Record to CONSTANT_CASE. * T: the type of the Record to transform. */ -type DeepPascalKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepPascalKeys } +type DeepConstantKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepConstantKeys } : T extends (infer V)[] - ? DeepPascalKeys[] + ? DeepConstantKeys[] : { - [K in keyof T as PascalCase>]: DeepPascalKeys + [K in keyof T as ConstantCase>]: DeepConstantKeys } /** - * Shallowly transforms the keys of an Record to PascalCase. - * T: the type of the Record to transform. - */ -type PascalKeys = T extends [] - ? T - : { [K in keyof T as PascalCase>]: T[K] } -/** - * 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. + * 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. * @param obj the object to transform. * @returns the transformed object. - * @example deepPascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { FizzBuzz: true } } + * @example deepConstantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { FIZZ_BUZZ: true } } */ -function deepPascalKeys(obj: T): DeepPascalKeys { - return deepTransformKeys(obj, toPascalCase) as never +function deepConstantKeys(obj: T): DeepConstantKeys { + return deepTransformKeys(obj, toConstantCase) as never } + /** - * 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. + * Shallowly transforms the keys of an Record to a custom delimiter case. + * T: the type of the Record to transform. + * D: the delimiter to use. + */ +type DelimiterKeys = T extends [] + ? T + : { [K in keyof T as DelimiterCase, D>]: T[K] } +/** + * 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. * @param obj the object to transform. + * @param delimiter the delimiter to use. * @returns the transformed object. - * @example pascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { 'fizz-buzz': true } } + * @example delimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } */ -function pascalKeys(obj: T): PascalKeys { - return transformKeys(obj, toPascalCase) as never +function delimiterKeys( + obj: T, + delimiter: D, +): DelimiterKeys { + return transformKeys(obj, (str) => toDelimiterCase(str, delimiter)) as never } /** - * Recursively transforms the keys of an Record to kebab-case. + * Recursively transforms the keys of an Record to a custom delimiter case. * T: the type of the Record to transform. + * D: the delimiter to use. */ -type DeepKebabKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepKebabKeys } +type DeepDelimiterKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepDelimiterKeys } : T extends (infer V)[] - ? DeepKebabKeys[] + ? DeepDelimiterKeys[] : { - [K in keyof T as KebabCase>]: DeepKebabKeys + [K in keyof T as DelimiterCase, D>]: DeepDelimiterKeys< + T[K], + D + > } +/** + * 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. + * @param obj the object to transform. + * @param delimiter the delimiter to use. + * @returns the transformed object. + * @example deepDelimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } + */ +function deepDelimiterKeys( + obj: T, + delimiter: D, +): DeepDelimiterKeys { + return deepTransformKeys(obj, (str) => + toDelimiterCase(str, delimiter), + ) as never +} + /** * Shallowly transforms the keys of an Record to kebab-case. * T: the type of the Record to transform. @@ -152,15 +196,6 @@ type KebabKeys = T extends [] : { [K in keyof T as KebabCase>]: T[K] } -/** - * 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. - * @param obj the object to transform. - * @returns the transformed object. - * @example deepKebabKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo-bar': { 'fizz-buzz': true } } - */ -function deepKebabKeys(obj: T): DeepKebabKeys { - return deepTransformKeys(obj, toKebabCase) as never -} /** * 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. * @param obj the object to transform. @@ -172,130 +207,100 @@ function kebabKeys(obj: T): KebabKeys { } /** - * Recursively transforms the keys of an Record to snake_case. + * Recursively transforms the keys of an Record to kebab-case. * T: the type of the Record to transform. */ -type DeepSnakeKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepSnakeKeys } +type DeepKebabKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepKebabKeys } : T extends (infer V)[] - ? DeepSnakeKeys[] + ? DeepKebabKeys[] : { - [K in keyof T as SnakeCase>]: DeepSnakeKeys + [K in keyof T as KebabCase>]: DeepKebabKeys } /** - * Shallowly transforms the keys of an Record to snake_case. - * T: the type of the Record to transform. - */ -type SnakeKeys = T extends [] - ? T - : { [K in keyof T as SnakeCase>]: T[K] } - -/** - * 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. + * 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. * @param obj the object to transform. * @returns the transformed object. - * @example deepSnakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz_buzz': true } } + * @example deepKebabKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo-bar': { 'fizz-buzz': true } } */ -function deepSnakeKeys(obj: T): DeepSnakeKeys { - return deepTransformKeys(obj, toSnakeCase) as never +function deepKebabKeys(obj: T): DeepKebabKeys { + return deepTransformKeys(obj, toKebabCase) as never } + /** - * A strongly typed function that shallowly the keys of an object to snake_case. The transformation is done both at runtime and type level. + * Shallowly transforms the keys of an Record to PascalCase. + * T: the type of the Record to transform. + */ +type PascalKeys = T extends [] + ? T + : { [K in keyof T as PascalCase>]: T[K] } +/** + * 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. * @param obj the object to transform. * @returns the transformed object. - * @example snakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz-buzz': true } } + * @example pascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { 'fizz-buzz': true } } */ -function snakeKeys(obj: T): SnakeKeys { - return transformKeys(obj, toSnakeCase) as never +function pascalKeys(obj: T): PascalKeys { + return transformKeys(obj, toPascalCase) as never } /** - * Recursively transforms the keys of an Record to CONSTANT_CASE. + * Recursively transforms the keys of an Record to PascalCase. * T: the type of the Record to transform. */ -type DeepConstantKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepConstantKeys } +type DeepPascalKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepPascalKeys } : T extends (infer V)[] - ? DeepConstantKeys[] + ? DeepPascalKeys[] : { - [K in keyof T as ConstantCase>]: DeepConstantKeys + [K in keyof T as PascalCase>]: DeepPascalKeys } /** - * Shallowly transforms the keys of an Record to CONSTANT_CASE. - * T: the type of the Record to transform. - */ -type ConstantKeys = T extends [] - ? T - : { [K in keyof T as ConstantCase>]: T[K] } -/** - * 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. + * 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. * @param obj the object to transform. * @returns the transformed object. - * @example deepConstantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { FIZZ_BUZZ: true } } + * @example deepPascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { FizzBuzz: true } } */ -function deepConstantKeys(obj: T): DeepConstantKeys { - return deepTransformKeys(obj, toConstantCase) as never +function deepPascalKeys(obj: T): DeepPascalKeys { + return deepTransformKeys(obj, toPascalCase) as never } + /** - * 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. + * Shallowly transforms the keys of an Record to snake_case. + * T: the type of the Record to transform. + */ +type SnakeKeys = T extends [] + ? T + : { [K in keyof T as SnakeCase>]: T[K] } +/** + * A strongly typed function that shallowly the keys of an object to snake_case. The transformation is done both at runtime and type level. * @param obj the object to transform. * @returns the transformed object. - * @example constantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { 'fizz-buzz': true } } + * @example snakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz-buzz': true } } */ -function constantKeys(obj: T): ConstantKeys { - return transformKeys(obj, toConstantCase) as never +function snakeKeys(obj: T): SnakeKeys { + return transformKeys(obj, toSnakeCase) as never } /** - * Recursively transforms the keys of an Record to a custom delimiter case. + * Recursively transforms the keys of an Record to snake_case. * T: the type of the Record to transform. - * D: the delimiter to use. */ -type DeepDelimiterKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepDelimiterKeys } +type DeepSnakeKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepSnakeKeys } : T extends (infer V)[] - ? DeepDelimiterKeys[] + ? DeepSnakeKeys[] : { - [K in keyof T as DelimiterCase, D>]: DeepDelimiterKeys< - T[K], - D - > + [K in keyof T as SnakeCase>]: DeepSnakeKeys } /** - * Shallowly transforms the keys of an Record to a custom delimiter case. - * T: the type of the Record to transform. - * D: the delimiter to use. - */ -type DelimiterKeys = T extends [] - ? T - : { [K in keyof T as DelimiterCase, D>]: T[K] } -/** - * 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. - * @param obj the object to transform. - * @param delimiter the delimiter to use. - * @returns the transformed object. - * @example deepDelimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } - */ -function deepDelimiterKeys( - obj: T, - delimiter: D, -): DeepDelimiterKeys { - return deepTransformKeys(obj, (str) => - toDelimiterCase(str, delimiter), - ) as never -} -/** - * 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. + * 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. * @param obj the object to transform. - * @param delimiter the delimiter to use. * @returns the transformed object. - * @example delimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } + * @example deepSnakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz_buzz': true } } */ -function delimiterKeys( - obj: T, - delimiter: D, -): DelimiterKeys { - return transformKeys(obj, (str) => toDelimiterCase(str, delimiter)) as never +function deepSnakeKeys(obj: T): DeepSnakeKeys { + return deepTransformKeys(obj, toSnakeCase) as never } export type { diff --git a/src/math.ts b/src/math.ts new file mode 100644 index 0000000..c6d3bad --- /dev/null +++ b/src/math.ts @@ -0,0 +1,27 @@ +import type { Length } from './primitives' + +type GetTuple< + L extends number, + result extends any[] = [], +> = result['length'] extends L ? result : GetTuple + +namespace Math { + export type Subtract< + A extends number, + B extends number, + > = GetTuple extends [...infer U, ...GetTuple] ? U['length'] : 0 + + export type IsPositive = `${T}` extends `-${number}` + ? false + : true + + export type Abs = + `${T}` extends `-${infer U extends number}` ? U : T + + export type GetPositiveIndex< + T extends string, + I extends number, + > = IsPositive extends true ? I : Subtract, Abs> +} + +export type { Math } diff --git a/src/primitives.test.ts b/src/primitives.test.ts index 8b0e2cf..733a231 100644 --- a/src/primitives.test.ts +++ b/src/primitives.test.ts @@ -24,6 +24,10 @@ namespace TypeTests { Equal, ['some', 'nice', 'string']> > type test8 = Expect, 'n'>> + type test9 = Expect< + Equal, 'nice string'> + > + type test10 = Expect, 16>> } describe('primitives', () => { @@ -38,8 +42,7 @@ describe('primitives', () => { describe('join', () => { test('should join words in both type level and runtime level', () => { - const data = ['a', 'b', 'c'] as const - const result = subject.join(data, '-') + const result = subject.join(['a', 'b', 'c'], '-') expect(result).toEqual('a-b-c') type test = Expect> }) @@ -52,6 +55,15 @@ describe('primitives', () => { }) }) + describe('length', () => { + test('should return the lenght of a string at both type level and runtime level', () => { + const data = 'some nice string' + const result = subject.length(data) + expect(result).toEqual(16) + type test = Expect> + }) + }) + test('replace', () => { test('should replace chars in a string at both type level and runtime level once', () => { const data = 'some nice string' @@ -70,6 +82,35 @@ describe('primitives', () => { }) }) + describe('slice', () => { + const str = 'The quick brown fox jumps over the lazy dog.' + test('should slice a string from a startIndex position', () => { + const result = subject.slice(str, 31) + expect(result).toEqual('the lazy dog.') + type test = Expect> + }) + + test('should slice a string from a startIndex to an endIndex position', () => { + const result = subject.slice(str, 4, 19) + expect(result).toEqual('quick brown fox') + type test = Expect> + }) + + test('should slice a string from the end with a negative startIndex', () => { + const result = subject.slice(str, -4) + expect(result).toEqual('dog.') + type test = Expect> + }) + + test('should slice a string from the end with a negative startIndex to a negative endIndex', () => { + const result = subject.slice(str, -9, -5) + expect(result).toEqual('lazy dog.') + type test = Expect> + // TODO: figure out how to deal with negative endIndex, this should be the expected result + // type test = Expect> + }) + }) + test('split', () => { test('should split a string by a delimiter into an array of substrings', () => { const data = 'some nice string' diff --git a/src/primitives.ts b/src/primitives.ts index a6baf85..c5a6b36 100644 --- a/src/primitives.ts +++ b/src/primitives.ts @@ -1,9 +1,11 @@ +import type { Math } from './math' + /** * Gets the character at the given index. * T: The string to get the character from. * index: The index of the character. */ -type CharAt = Split[index] +type CharAt = Split[index] /** * A strongly typed version of `String.prototype.charAt`. * @param str the string to get the character from. @@ -51,6 +53,20 @@ function join( return tuple.join(delimiter ?? ('' as const)) as Join } +/** + * Gets the length of a string. + */ +type Length = Split['length'] +/** + * A strongly typed version of `String.prototype.length`. + * @param str the string to get the length from. + * @returns the length of the string in both type level and runtime. + * @example length('hello world') // 11 + */ +function length(str: T) { + return str.length as Length +} + /** * Replaces the first occurrence of a string with another string. * sentence: The sentence to replace. @@ -119,6 +135,53 @@ function replaceAll( ) as ReplaceAll } +// TODO: this is not equivalent to the native slice but it is as far as I got with Type level arithmetic. When the startIndex is negative, the endIndex is gonna be considered as undefined. +/** + * Slices a string from a startIndex to an endIndex. + * T: The string to slice. + * startIndex: The start index. + * endIndex: The end index. + * @warning 🚨 it doesn't work exactly like the native slice as it will ignore the end index if start index is negative + */ +type Slice< + T extends string, + startIndex extends number = 0, + endIndex extends number = Split['length'], +> = T extends `${infer head}${infer rest}` + ? startIndex extends 0 + ? endIndex extends 0 + ? '' + : `${head}${Slice< + rest, + 0, + endIndex extends -1 ? -1 : Math.Subtract + >}` + : `${Slice< + rest, + Math.Subtract, 1>, + Math.IsPositive extends true + ? Math.Subtract + : Split['length'] // TODO: figure out how to deal with negative endIndex + >}` + : '' +/** + * A strongly typed version of `String.prototype.slice`. + * @param str the string to slice. + * @param start the start index. + * @param end the end index. + * @returns the sliced string in both type level and runtime. + * @example slice('hello world', 6) // 'world' + * @warning 🚨 it doesn't work exactly like the native slice as it will ignore the end index if start index is negative + */ +function slice< + T extends string, + const S extends number = 0, + const E extends number = Split['length'], +>(str: T, start: S = 0 as S, end: E = str.length as E) { + // TODO: figure out how to deal with negative endIndex + return str.slice(start, start < 0 ? undefined : end) as Slice +} + /** * Splits a string into an array of substrings. * T: The string to split. @@ -194,11 +257,24 @@ function trim(str: T) { export type { CharAt, Join, + Length, Replace, ReplaceAll, + Slice, Split, TrimStart, TrimEnd, Trim, } -export { charAt, join, replace, replaceAll, split, trim, trimStart, trimEnd } +export { + charAt, + join, + length, + replace, + replaceAll, + slice, + split, + trim, + trimStart, + trimEnd, +} diff --git a/src/utils.ts b/src/utils.ts index 5b2b1ca..84a0ac2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -58,7 +58,6 @@ type IsSpecial = IsLetter extends true : true // STRING FUNCTIONS - /** * Splits a string into words. * sentence: The current string to split.