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. You end up losing type information and possibly having to cast the result.
const str = 'hello-world'
const result = str.replace('-', ' ') // you should use: as 'hello world'
// ^? string
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.
import { replace } from 'string-ts'
const str = 'hello-world'
const result = replace(str, '-', ' ')
// ^ 'hello world'
TypeScript yields the best static analysis when types are highly specific.
Literals are more specific than type string
.
This library preserves literals (and unions of literals) after transformations, unlike most existing utility libraries (and built-in string methods.)
I still don't get the purpose of this library π€
In the below example, I want to get a strongly-typed, camel-case version of process.env
.
One flow results in a loose type, and the other results in a more precise type.
This example should illustrate the highly-specific and flexible nature of string-ts
.
import { deepCamelKeys } from 'string-ts'
import { camelCase, mapKeys } from 'lodash-es'
import z from 'zod'
const EnvSchema = z.object({
NODE_ENV: z.string(),
})
function getEnvLoose() {
const rawEnv = EnvSchema.parse(process.env)
const env = mapKeys(rawEnv, (_v, k) => camelCase(k))
// ^? Dictionary<string>
// `Dictionary<string>` is too loose
// TypeScript is okay with this, 'abc' is expected to be of type `string`
// This will have unexpected behavior at runtime
console.log(env.abc)
}
function getEnvPrecise() {
const rawEnv = EnvSchema.parse(process.env)
const env = deepCamelKeys(rawEnv)
// ^? { nodeEnv: string }
// Error: Property 'abc' does not exist on type '{ nodeEnv: string; }'
// Our type is more specific, so TypeScript catches this error.
// This mistake will be caught at compile time
console.log(env.abc)
}
function main() {
getEnvLoose()
getEnvPrecise()
}
main()
npm install string-ts
string-ts
has been designed with tree shaking in mind.
We have tested it with build tools like Webpack, Vite, Rollup, etc.
string-ts
currently only works on TypeScript v5+.
It also only work with common ASCII characters characters. We don't plan to support international characters or emojis.
- Runtime counterparts of native type utilities
- Strongly-typed alternatives to native runtime utilities
- Strongly-typed alternatives to common loosely-typed functions
- Strongly-typed shallow transformation of objects
- Strongly-typed deep transformation of objects
- Type Utilities
- Runtime-only utilities
Capitalizes the first letter of a string. This is a runtime counterpart of Capitalize<T>
from src/types.d.ts
.
import { capitalize } from 'string-ts'
const str = 'hello world'
const result = capitalize(str)
// ^ 'Hello world'
Uncapitalizes the first letter of a string. This is a runtime counterpart of Uncapitalize<T>
from src/types.d.ts
.
import { uncapitalize } from 'string-ts'
const str = 'Hello world'
const result = uncapitalize(str)
// ^ 'hello world'
This function is a strongly-typed counterpart of String.prototype.charAt
.
import { charAt } from 'string-ts'
const str = 'hello world'
const result = charAt(str, 6)
// ^ 'w'
This function is a strongly-typed counterpart of String.prototype.concat
.
import { concat } from 'string-ts'
const result = concat('a', 'bc', 'def')
// ^ 'abcdef'
This function is a strongly-typed counterpart of String.prototype.endsWith
.
import { endsWith } from 'string-ts'
const result = endsWith('abc', 'c')
// ^ true
This function is a strongly-typed counterpart of String.prototype.includes
.
import { includes } from 'string-ts'
const result = includes('abcde', 'bcd')
// ^ true
This function is a strongly-typed counterpart of Array.prototype.join
.
import { join } from 'string-ts'
const str = ['hello', 'world']
const result = join(str, ' ')
// ^ 'hello world'
This function is a strongly-typed counterpart of String.prototype.length
.
import { length } from 'string-ts'
const str = 'hello'
const result = length(str)
// ^ 5
This function is a strongly-typed counterpart of String.prototype.padEnd
.
import { padEnd } from 'string-ts'
const str = 'hello'
const result = padEnd(str, 10, '=')
// ^ 'hello====='
This function is a strongly-typed counterpart of String.prototype.padStart
.
import { padStart } from 'string-ts'
const str = 'hello'
const result = padStart(str, 10, '=')
// ^ '=====hello'
This function is a strongly-typed counterpart of String.prototype.repeat
.
import { repeat } from 'string-ts'
const str = 'abc'
const result = repeat(str, 3)
// ^ 'abcabcabc'
This function is a strongly-typed counterpart of String.prototype.replace
.
Warning: this is a partial implementation, as we don't fully support Regex. Using a RegExp lookup will result in a loose typing.
import { replace } from 'string-ts'
const str = 'hello-world-'
const result = replace(str, '-', ' ')
// ^ 'hello world-'
const looselyTypedResult = replace(str, /-/, ' ')
// ^ string
This function is a strongly-typed counterpart of String.prototype.replaceAll
.
It also has a polyfill for runtimes older than ES2021.
Warning: this is a partial implementation, as we don't fully support Regex. Using a RegExp lookup will result in a loose typing.
import { replaceAll } from 'string-ts'
const str = 'hello-world-'
const result = replaceAll(str, '-', ' ')
// ^ 'hello world '
const looselyTypedResult = replaceAll(str, /-/g, ' ')
// ^ string
This function is a strongly-typed counterpart of String.prototype.slice
.
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'
This function is a strongly-typed counterpart of String.prototype.split
.
import { split } from 'string-ts'
const str = 'hello-world'
const result = split(str, '-')
// ^ ['hello', 'world']
This function is a strongly-typed counterpart of String.prototype.startsWith
.
import { startsWith } from 'string-ts'
const result = startsWith('abc', 'a')
// ^ true
This function is a strongly-typed counterpart of String.prototype.toLowerCase
.
import { toLowerCase } from 'string-ts'
const str = 'HELLO WORLD'
const result = toLowerCase(str)
// ^ 'hello world'
This function is a strongly-typed counterpart of String.prototype.toUpperCase
.
import { toUpperCase } from 'string-ts'
const str = 'hello world'
const result = toUpperCase(str)
// ^ 'HELLO WORLD'
This function is a strongly-typed counterpart of String.prototype.trim
.
import { trim } from 'string-ts'
const str = ' hello world '
const result = trim(str)
// ^ 'hello world'
This function is a strongly-typed counterpart of String.prototype.trimEnd
.
import { trimEnd } from 'string-ts'
const str = ' hello world '
const result = trimEnd(str)
// ^ ' hello world'
This function is a strongly-typed counterpart of String.prototype.trimStart
.
import { trimStart } from 'string-ts'
const str = ' hello world '
const result = trimStart(str)
// ^ 'hello world '
This function converts a string to lower case
at both runtime and type levels.
NOTE: this function will split by words and join them with " "
, unlike toLowerCase
.
import { lowerCase } from 'string-ts'
const str = 'HELLO-WORLD'
const result = lowerCase(str)
// ^ 'hello world'
This function converts a string to camelCase
at both runtime and type levels.
import { camelCase } from 'string-ts'
const str = 'hello-world'
const result = camelCase(str)
// ^ 'helloWorld'
This function converts a string to CONSTANT_CASE
at both runtime and type levels.
import { constantCase } from 'string-ts'
const str = 'helloWorld'
const result = constantCase(str)
// ^ 'HELLO_WORLD'
This function converts a string to a new case with a custom delimiter at both runtime and type levels.
import { delimiterCase } from 'string-ts'
const str = 'helloWorld'
const result = delimiterCase(str, '.')
// ^ 'hello.World'
This function converts a string to kebab-case
at both runtime and type levels.
import { kebabCase } from 'string-ts'
const str = 'helloWorld'
const result = kebabCase(str)
// ^ 'hello-world'
This function converts a string to PascalCase
at both runtime and type levels.
import { pascalCase } from 'string-ts'
const str = 'hello-world'
const result = pascalCase(str)
// ^ 'HelloWorld'
This function converts a string to snake_case
at both runtime and type levels.
import { snakeCase } from 'string-ts'
const str = 'helloWorld'
const result = snakeCase(str)
// ^ 'hello_world'
This function converts a string to Title Case
at both runtime and type levels.
import { titleCase } from 'string-ts'
const str = 'helloWorld'
const result = titleCase(str)
// ^ 'Hello World'
This function converts a string to UPPER CASE
at both runtime and type levels.
NOTE: this function will split by words and join them with " "
, unlike toUpperCase
.
import { upperCase } from 'string-ts'
const str = 'hello-world'
const result = upperCase(str)
// ^ 'HELLO WORLD'
This function reverses a string.
import { reverse } from 'string-ts'
const str = 'Hello StringTS!'
const result = reverse(str)
// ^ '!TSgnirtS olleH'
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 "...".
import { truncate } from 'string-ts'
const str = '-20someVery-weird String'
const result = truncate(str, 8)
// ^ '-20so...'
This function identifies the words in a string and returns a tuple of words split by separators, differences in casing, numbers, and etc.
import { words } from 'string-ts'
const str = '-20someVery-weird String'
const result = words(str)
// ^ ['20', 'some', 'Very', 'weird', 'String']
This function shallowly converts the keys of an object to camelCase
at both runtime and type levels.
import { camelKeys } from 'string-ts'
const data = {
'hello-world': {
'foo-bar': 'baz',
},
} as const
const result = camelKeys(data)
// ^ { helloWorld: { 'foo-bar': 'baz' } }
This function shallowly converts the keys of an object to CONSTANT_CASE
at both runtime and type levels.
import { constantKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = constantKeys(data)
// ^ { 'HELLO_WORLD': { 'fooBar': 'baz' } }
This function shallowly converts the keys of an object to a new case with a custom delimiter at both runtime and type levels.
import { delimiterKeys } from 'string-ts'
const data = {
'hello-world': {
'foo-bar': 'baz',
},
} as const
const result = delimiterKeys(data, '.')
// ^ { 'hello.world': { 'foo-bar': 'baz' } }
This function shallowly converts the keys of an object to kebab-case
at both runtime and type levels.
import { kebabKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = kebabKeys(data)
// ^ { 'hello-world': { fooBar: 'baz' } }
This function shallowly converts the keys of an object to PascalCase
at both runtime and type levels.
import { pascalKeys } from 'string-ts'
const data = {
'hello-world': {
'foo-bar': 'baz',
},
} as const
const result = pascalKeys(data)
// ^ { HelloWorld: { FooBar: 'baz' } }
This function shallowly converts the keys of an object to snake_case
at both runtime and type levels.
import { snakeKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = snakeKeys(data)
// ^ { 'hello_world': { 'fooBar': 'baz' } }
This function shallowly transforms the keys of an object by applying replace
to each of its keys at both runtime and type levels.
import { replaceKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = replaceKeys(data, 'o', 'a')
// ^ { 'hellaWorld': { 'fooBar': 'baz' } }
This function recursively converts the keys of an object to camelCase
at both runtime and type levels.
import { deepCamelKeys } from 'string-ts'
const data = {
'hello-world': {
'foo-bar': 'baz',
},
} as const
const result = deepCamelKeys(data)
// ^ { helloWorld: { fooBar: 'baz' } }
This function recursively converts the keys of an object to CONSTANT_CASE
at both runtime and type levels.
import { deepConstantKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = deepConstantKeys(data)
// ^ { 'HELLO_WORLD': { 'FOO_BAR': 'baz' } }
This function recursively converts the keys of an object to a new case with a custom delimiter at both runtime and type levels.
import { deepDelimiterKeys } from 'string-ts'
const data = {
'hello-world': {
'foo-bar': 'baz',
},
} as const
const result = deepDelimiterKeys(data, '.')
// ^ { 'hello.world': { 'foo.bar': 'baz' } }
This function recursively converts the keys of an object to kebab-case
at both runtime and type levels.
import { deepKebabKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = deepKebabKeys(data)
// ^ { 'hello-world': { 'foo-bar': 'baz' } }
This function recursively converts the keys of an object to PascalCase
at both runtime and type levels.
import { deepPascalKeys } from 'string-ts'
const data = {
'hello-world': {
'foo-bar': 'baz',
},
} as const
const result = deepPascalKeys(data)
// ^ { HelloWorld: { FooBar: 'baz' } }
This function recursively converts the keys of an object to snake_case
at both runtime and type levels.
import { deepSnakeKeys } from 'string-ts'
const data = {
helloWorld: {
fooBar: 'baz',
},
} as const
const result = deepSnakeKeys(data)
// ^ { 'hello_world': { 'foo_bar': 'baz' } }
All the functions presented in this API have associated type counterparts.
import type * as St from 'string-ts'
Capitalize<'hello world'> // 'Hello world'
Lowercase<'HELLO WORLD'> // 'hello world'
Uppercase<'hello world'> // 'HELLO WORLD'
St.CharAt<'hello world', 6> // 'w'
St.Concat<['a', 'bc', 'def']> // 'abcdef'
St.EndsWith<'abc', 'c'> // true
St.Includes<'abcde', 'bcd'> // true
St.Join<['hello', 'world'], '-'> // 'hello-world'
St.Length<'hello'> // 5
St.PadEnd<'hello', 10, '='> // 'hello====='
St.PadStart<'hello', 10, '='> // '=====hello'
St.Repeat<'abc', 3> // 'abcabcabc'
St.Replace<'hello-world', 'l', '1'> // 'he1lo-world'
St.ReplaceAll<'hello-world', 'l', '1'> // 'he11o-wor1d'
St.Reverse<'Hello World!'> // '!dlroW olleH'
St.Slice<'hello-world', -5> // 'world'
St.Split<'hello-world', '-'> // ['hello', 'world']
St.Trim<' hello world '> // 'hello world'
St.StartsWith<'abc', 'a'> // true
St.TrimEnd<' hello world '> // ' hello world'
St.TrimStart<' hello world '> // 'hello world '
St.Truncate<'hello world', 9, '[...]'> // 'hello[...]
St.Words<'hello-world'> // ['hello', 'world']
St.CamelCase<'hello-world'> // 'helloWorld'
St.ConstantCase<'helloWorld'> // 'HELLO_WORLD'
St.DelimiterCase<'hello world', '.'> // 'hello.world'
St.KebabCase<'helloWorld'> // 'hello-world'
St.PascalCase<'hello-world'> // 'HelloWorld'
St.SnakeCase<'helloWorld'> // 'hello_world'
St.TitleCase<'helloWorld'> // 'Hello World'
Note that we do not include UpperCase
and LowerCase
types. These would be too close to the existing TS types Uppercase
and Lowercase
.
One could create either by doing like so:
type LowerCase<T extends string> = Lowercase<DelimiterCase<T, ' '>>
type UpperCase<T extends string> = Uppercase<DelimiterCase<T, ' '>>
// or
type LowerCase<T extends string> = ReturnType<typeof lowerCase<T>>
type UpperCase<T extends string> = ReturnType<typeof upperCase<T>>
St.CamelKeys<{
'hello-world': { 'foo-bar': 'baz' }
}> // { helloWorld: { 'foo-bar': 'baz' } }
St.ConstantKeys<{
helloWorld: { fooBar: 'baz' }
}> // { 'HELLO_WORLD': { fooBar: 'baz' } }
St.DelimiterKeys<{ 'hello-world': { 'foo-bar': 'baz' } }, '.'>
// { 'hello.world': { 'foo-bar': 'baz' } }
St.KebabKeys<{
helloWorld: { fooBar: 'baz' }
}> // { 'hello-world': { fooBar: 'baz' } }
St.PascalKeys<{
'hello-world': { 'foo-bar': 'baz' }
}> // { HelloWorld: { 'foo-bar': 'baz' } }
St.SnakeKeys<{
helloWorld: { fooBar: 'baz' }
}> // { 'hello_world': { fooBar: 'baz' } }
St.DeepCamelKeys<{
'hello-world': { 'foo-bar': 'baz' }
}> // { helloWorld: { fooBar: 'baz' } }
St.DeepConstantKeys<{
helloWorld: { fooBar: 'baz' }
}> // { 'HELLO_WORLD': { 'FOO_BAR': 'baz' } }
St.DeepDelimiterKeys<
{
'hello-world': { 'foo-bar': 'baz' }
},
'.'
> // { 'hello.world': { 'foo.bar': 'baz' } }
St.DeepKebabKeys<{
helloWorld: { fooBar: 'baz' }
}> // { 'hello-world': { 'foo-bar': 'baz' } }
St.DeepPascalKeys<{
'hello-world': { 'foo-bar': 'baz' }
}> // { HelloWorld: { FooBar: 'baz' } }
St.DeepSnakeKeys<{
helloWorld: { fooBar: 'baz' }
}> // { 'hello_world': { 'foo_bar': 'baz' } }
St.IsDigit<'a'> // false
St.IsDigit<'1'> // true
St.IsLetter<'a'> // true
St.IsLetter<'1'> // false
St.IsLower<'a'> // true
St.IsLower<'A'> // false
St.IsUpper<'a'> // false
St.IsUpper<'A'> // true
St.IsSeparator<' '> // true
St.IsSeparator<'-'> // true
St.IsSeparator<'a'> // false
St.IsSpecial<'a'> // false
St.IsSpecial<'!'> // true
St.IsSpecial<' '> // false
This function recursively converts the keys of an object to a custom format, but only at runtime level.
import { deepTransformKeys, toUpperCase } from 'string-ts'
const data = { helloWorld: 'baz' } as const
type MyType<T> = { [K in keyof T as Uppercase<K>]: T[K] }
const result = deepTransformKeys(data, toUpperCase) as MyType<typeof data>
// ^ { 'HELLOWORLD': 'baz' }
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.
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
StringTs logo by NUMI:
This library got a lot of inspiration from libraries such as lodash, ts-reset, type-fest, HOTScript, and many others.