String
The String module gives you Effect-style helpers for working with plain
JavaScript strings. There is no wrapper type — a String value is just a
string — but the functions are data-last (curried) so they drop straight
into pipe and flow, and several of them preserve string literal types at
the type level (Capitalize, Trim, Concat).
Operations that can miss — at, indexOf, match, search, and friends —
return an Option instead of the native sentinels
(-1, undefined, null), so a “not found” result is a value you handle
explicitly rather than a magic number you have to remember to check.
import { pipe, String } from "effect"
// Data-last helpers chain cleanly in a pipeconst tags = pipe( " TypeScript, Effect, Schema ", String.trim, // "TypeScript, Effect, Schema" String.toLowerCase, // "typescript, effect, schema" String.split(", ") // ["typescript", "effect", "schema"])console.log(tags) // ["typescript", "effect", "schema"]Because the helpers are curried, the single-argument call (String.split(", "))
returns a function (self: string) => ..., which is exactly what pipe feeds
the previous result into. Every helper also has a data-first overload where it
makes sense, so String.split("a,b", ",") works too.
Literal types are preserved
Section titled “Literal types are preserved”A handful of transforms thread the literal type through, so the compiler knows the exact resulting string when the input is a literal:
import { String } from "effect"
const tag = String.capitalize("button") // type: "Button", value: "Button"const trimmed = String.trim(" hi ") // type: "hi", value: "hi"
// Type-level only, mirroring the runtime functions:type T1 = String.Concat<"foo", "bar"> // "foobar"type T2 = String.Trim<" hi "> // "hi"type T3 = Capitalize<"button"> // built-in TS intrinsic, same ideaBasics: the namespace and the empty string
Section titled “Basics: the namespace and the empty string”String.String is literally the global String constructor, re-exported so you
can reach it without leaving the Effect namespace, and String.empty is the
canonical "".
import { String } from "effect"
console.log(String.String === globalThis.String) // trueconsole.log(String.String(123)) // "123" (native coercion)
console.log(String.empty) // ""console.log(String.isEmpty(String.empty)) // trueReference
Section titled “Reference”Every public export of the module, grouped by task. All examples are runnable.
Constants, instances & guards
Section titled “Constants, instances & guards”String
Section titled “String”The native globalThis.String constructor, re-exported. String.String(value)
performs native string coercion; new String.String(value) builds a boxed
object (rarely what you want).
import { String } from "effect"
console.log(String.String(true)) // => "true"console.log(String.String([1, 2])) // => "1,2"The empty string "", typed as the literal "".
import { String } from "effect"
console.log(String.empty) // => ""isString
Section titled “isString”A Refinement<unknown, string> that narrows unknown input to string.
import { String } from "effect"
console.log(String.isString("a")) // => trueconsole.log(String.isString(1)) // => falseAn Order<string> using lexicographic comparison. Returns
-1, 0, or 1.
import { String } from "effect"
console.log(String.Order("apple", "banana")) // => -1console.log(String.Order("banana", "apple")) // => 1console.log(String.Order("apple", "apple")) // => 0Equivalence
Section titled “Equivalence”An Equivalence<string> using strict === equality.
import { String } from "effect"
console.log(String.Equivalence("hello", "hello")) // => trueconsole.log(String.Equivalence("hello", "world")) // => falseReducerConcat
Section titled “ReducerConcat”A Reducer<string> that concatenates strings, starting from "". Use it with
APIs that consume a Reducer to fold many strings into one.
import { String } from "effect"
console.log(String.ReducerConcat.combineAll(["a", "b", "c"])) // => "abc"console.log(String.ReducerConcat.combineAll([])) // => ""Concatenation & length
Section titled “Concatenation & length”concat
Section titled “concat”Concatenates two strings. The type-level Concat<A, B> produces the joined
literal type when both inputs are literals.
import { pipe, String } from "effect"
console.log(String.concat("hello", "world")) // => "helloworld"console.log(pipe("hello", String.concat("world"))) // => "helloworld"
// Type-level equivalent:type R = String.Concat<"hello", "world"> // "helloworld"length
Section titled “length”The JavaScript string length, measured in UTF-16 code units (not user-perceived characters).
import { String } from "effect"
console.log(String.length("abc")) // => 3isEmpty
Section titled “isEmpty”Returns true for "". Narrows the type to "".
import { String } from "effect"
console.log(String.isEmpty("")) // => trueconsole.log(String.isEmpty("a")) // => falseisNonEmpty
Section titled “isNonEmpty”Returns true when the string has at least one code unit.
import { String } from "effect"
console.log(String.isNonEmpty("")) // => falseconsole.log(String.isNonEmpty("a")) // => trueCase transforms (type-preserving)
Section titled “Case transforms (type-preserving)”These thread the literal type through using TypeScript’s intrinsic string types,
so toUpperCase("ab") has type "AB".
toUpperCase
Section titled “toUpperCase”Uppercases the whole string; result type is Uppercase<S>.
import { pipe, String } from "effect"
console.log(String.toUpperCase("hello")) // => "HELLO"console.log(pipe("a", String.toUpperCase)) // => "A"toLowerCase
Section titled “toLowerCase”Lowercases the whole string; result type is Lowercase<S>.
import { String } from "effect"
console.log(String.toLowerCase("HELLO")) // => "hello"capitalize
Section titled “capitalize”Uppercases the first character only; result type is Capitalize<S>.
import { String } from "effect"
console.log(String.capitalize("hello")) // => "Hello"uncapitalize
Section titled “uncapitalize”Lowercases the first character only; result type is Uncapitalize<S>.
import { String } from "effect"
console.log(String.uncapitalize("Hello")) // => "hello"toLocaleLowerCase
Section titled “toLocaleLowerCase”Lowercases according to a locale (handles cases the locale-agnostic version
gets wrong, e.g. Turkish dotted/dotless i).
import { pipe, String } from "effect"
console.log(pipe("İ", String.toLocaleLowerCase("tr"))) // => "i"toLocaleUpperCase
Section titled “toLocaleUpperCase”Uppercases according to a locale.
import { pipe, String } from "effect"
console.log(pipe("i̇", String.toLocaleUpperCase("lt-LT"))) // => "I"Trimming (type-level Trim)
Section titled “Trimming (type-level Trim)”The runtime trims behave like native methods; the Trim, TrimStart, and
TrimEnd type-level helpers compute the trimmed literal at compile time.
Removes whitespace from both ends. Result type is Trim<A>.
import { String } from "effect"
console.log(String.trim(" hello world ")) // => "hello world"
type R = String.Trim<" hi "> // "hi"trimStart
Section titled “trimStart”Removes leading whitespace. Result type is TrimStart<A>.
import { String } from "effect"
console.log(String.trimStart(" hello world")) // => "hello world"console.log(String.trimStart(" a ")) // => "a "trimEnd
Section titled “trimEnd”Removes trailing whitespace. Result type is TrimEnd<A>.
import { String } from "effect"
console.log(String.trimEnd("hello world ")) // => "hello world"console.log(String.trimEnd(" a ")) // => " a"Slicing & indexing
Section titled “Slicing & indexing”Extracts a section between two indices (negative indices count from the end).
import { pipe, String } from "effect"
console.log(pipe("abcd", String.slice(1, 3))) // => "bc"console.log(pipe("hello world", String.slice(0, 5))) // => "hello"console.log(pipe("abcd", String.slice(-2))) // => "cd"substring
Section titled “substring”Extracts characters between start and (optional) end. Unlike slice,
negative arguments are treated as 0.
import { pipe, String } from "effect"
console.log(pipe("abcd", String.substring(1))) // => "bcd"console.log(pipe("abcd", String.substring(1, 3))) // => "bc"The character at a (possibly negative) index, as Option. None when out of
bounds — no undefined to forget about.
import { pipe, String } from "effect"
console.log(pipe("abc", String.at(1))) // => Option.some("b")console.log(pipe("abc", String.at(-1))) // => Option.some("c")console.log(pipe("abc", String.at(4))) // => Option.none()charAt
Section titled “charAt”The character at a non-negative index, as Option. None when out of bounds.
import { pipe, String } from "effect"
console.log(pipe("abc", String.charAt(1))) // => Option.some("b")console.log(pipe("abc", String.charAt(4))) // => Option.none()charCodeAt
Section titled “charCodeAt”The UTF-16 code unit at an index, as Option<number>. None instead of NaN
when out of bounds.
import { String } from "effect"
console.log(String.charCodeAt("abc", 1)) // => Option.some(98)console.log(String.charCodeAt("abc", 4)) // => Option.none()codePointAt
Section titled “codePointAt”The Unicode code point at an index, as Option<number>. None instead of
undefined when out of bounds.
import { pipe, String } from "effect"
console.log(pipe("abc", String.codePointAt(1))) // => Option.some(98)console.log(pipe("abc", String.codePointAt(10))) // => Option.none()takeLeft
Section titled “takeLeft”Keeps the first n characters. Clamps: n <= 0 gives "", oversized n gives
the whole string, floats round down.
import { String } from "effect"
console.log(String.takeLeft("Hello World", 5)) // => "Hello"console.log(String.takeLeft("Hi", 100)) // => "Hi"takeRight
Section titled “takeRight”Keeps the last n characters, with the same clamping rules as takeLeft.
import { String } from "effect"
console.log(String.takeRight("Hello World", 5)) // => "World"console.log(String.takeRight("Hi", 0)) // => ""Search & test
Section titled “Search & test”Predicates (includes, startsWith, endsWith) return plain boolean; the
positional searches return Option so a miss is never -1 or null.
includes
Section titled “includes”true if searchString appears at or after the optional position.
import { pipe, String } from "effect"
console.log(pipe("hello world", String.includes("world"))) // => trueconsole.log(pipe("hello world", String.includes("foo"))) // => falsestartsWith
Section titled “startsWith”true if the string starts with the search string (optionally from a position).
import { pipe, String } from "effect"
console.log(pipe("hello world", String.startsWith("hello"))) // => trueconsole.log(pipe("hello world", String.startsWith("world"))) // => falseendsWith
Section titled “endsWith”true if the string ends with the search string (optionally up to a position).
import { pipe, String } from "effect"
console.log(pipe("hello world", String.endsWith("world"))) // => trueconsole.log(pipe("hello world", String.endsWith("hello"))) // => falseindexOf
Section titled “indexOf”Index of the first occurrence, as Option<number> — None instead of -1.
import { pipe, String } from "effect"
console.log(pipe("abbbc", String.indexOf("b"))) // => Option.some(1)console.log(pipe("abbbc", String.indexOf("z"))) // => Option.none()lastIndexOf
Section titled “lastIndexOf”Index of the last occurrence, as Option<number> — None instead of -1.
import { pipe, String } from "effect"
console.log(pipe("abbbc", String.lastIndexOf("b"))) // => Option.some(3)console.log(pipe("abbbc", String.lastIndexOf("d"))) // => Option.none()search
Section titled “search”Index of the first match for a string or RegExp, as Option<number> — None
instead of -1.
import { String } from "effect"
console.log(String.search("ababb", "b")) // => Option.some(1)console.log(String.search("ababb", /abb/)) // => Option.some(2)console.log(String.search("ababb", "d")) // => Option.none()Matches against a pattern, returning Option<RegExpMatchArray> — None instead
of null when there is no match.
import { Option, pipe, String } from "effect"
const m = pipe("hello", String.match(/l+/))if (Option.isSome(m)) { console.log(`${m.value[0]}@${m.value.index}`) // => "ll@2"}console.log(Option.isNone(pipe("hello", String.match(/x/)))) // => truematchAll
Section titled “matchAll”Returns an iterator over all matches for a global RegExp, using native
matchAll semantics.
import { pipe, String } from "effect"
const matches = pipe("hello world", String.matchAll(/l/g))console.log( Array.from(matches, (match) => `${match[0]}@${match.index}`).join(", ")) // => "l@2, l@3, l@9"localeCompare
Section titled “localeCompare”Locale-aware comparison returning an Ordering (-1, 0, or 1), with
optional locales and Intl.CollatorOptions.
import { pipe, String } from "effect"
console.log(pipe("a", String.localeCompare("b"))) // => -1console.log(pipe("b", String.localeCompare("a"))) // => 1console.log(pipe("a", String.localeCompare("a"))) // => 0Replace, split & build
Section titled “Replace, split & build”replace
Section titled “replace”Replaces the first match (string or non-global RegExp); a global RegExp
replaces every match. Follows native String.prototype.replace.
import { pipe, String } from "effect"
console.log(pipe("abc", String.replace("b", "d"))) // => "adc"console.log(pipe("a-a-a", String.replace("-", "_"))) // => "a_a-a"console.log(pipe("a-a-a", String.replace(/-/g, "_"))) // => "a_a_a"replaceAll
Section titled “replaceAll”Replaces all occurrences of a substring or pattern.
import { pipe, String } from "effect"
console.log(pipe("ababb", String.replaceAll("b", "c"))) // => "acacc"console.log(pipe("ababb", String.replaceAll(/ba/g, "cc"))) // => "accbb"Splits into a non-empty array of substrings. Splitting "" yields [""],
so the result is always at least one element.
import { pipe, String } from "effect"
console.log(String.split("hello,world", ",")) // => ["hello", "world"]console.log(pipe("abc", String.split(""))) // => ["a", "b", "c"]console.log(pipe("", String.split(""))) // => [""]repeat
Section titled “repeat”Repeats the string count times.
import { pipe, String } from "effect"
console.log(pipe("a", String.repeat(5))) // => "aaaaa"console.log(pipe("ab", String.repeat(3))) // => "ababab"padStart
Section titled “padStart”Pads from the start to maxLength with an optional fill string (default " ").
import { pipe, String } from "effect"
console.log(pipe("a", String.padStart(5))) // => " a"console.log(pipe("7", String.padStart(3, "0"))) // => "007"padEnd
Section titled “padEnd”Pads from the end to maxLength with an optional fill string (default " ").
import { pipe, String } from "effect"
console.log(pipe("a", String.padEnd(5))) // => "a "console.log(pipe("a", String.padEnd(5, "_"))) // => "a____"normalize
Section titled “normalize”Normalizes to a Unicode normalization form: "NFC" (default), "NFD",
"NFKC", or "NFKD". Use it before comparing strings that may use combining
characters.
import { pipe, String } from "effect"
const str = "ẛ̣"console.log(pipe(str, String.normalize("NFC")) === str) // => trueconsole.log(pipe(str, String.normalize("NFKC"))) // => "ṩ"linesIterator
Section titled “linesIterator”An iterable over each line, stripping the trailing newline. Handles \n,
\r, and \r\n.
import { String } from "effect"
console.log(Array.from(String.linesIterator("hello\nworld\n")))// => ["hello", "world"]linesWithSeparators
Section titled “linesWithSeparators”An iterable over each line, keeping the trailing newline characters.
import { String } from "effect"
console.log(Array.from(String.linesWithSeparators("hello\nworld\n")))// => ["hello\n", "world\n"]stripMargin
Section titled “stripMargin”Strips a leading | margin from every line (after any leading whitespace).
Handy for multi-line template literals.
import { String } from "effect"
console.log(String.stripMargin(" |hello\n |world"))// => "hello\nworld"stripMarginWith
Section titled “stripMarginWith”Like stripMargin, but you choose the margin character.
import { String } from "effect"
console.log(String.stripMarginWith(" $hello\n $world", "$"))// => "hello\nworld"Case-style conversions
Section titled “Case-style conversions”There are two families here. The fixed converters (snakeToCamel,
camelToSnake, …) assume their input already follows the named source shape and
are cheap, direct transforms. The general converters (camelCase,
snakeCase, kebabCase, pascalCase, constantCase, and the configurable
noCase) tokenize arbitrary mixed input — spaces, separators, and case
boundaries — so they are the right choice for free-form text.
snakeToCamel
Section titled “snakeToCamel”snake_case to camelCase. Assumes snake_case input.
import { String } from "effect"
console.log(String.snakeToCamel("hello_world")) // => "helloWorld"console.log(String.snakeToCamel("foo_bar_baz")) // => "fooBarBaz"snakeToPascal
Section titled “snakeToPascal”snake_case to PascalCase.
import { String } from "effect"
console.log(String.snakeToPascal("hello_world")) // => "HelloWorld"console.log(String.snakeToPascal("foo_bar_baz")) // => "FooBarBaz"snakeToKebab
Section titled “snakeToKebab”snake_case to kebab-case (swaps _ for -).
import { String } from "effect"
console.log(String.snakeToKebab("hello_world")) // => "hello-world"console.log(String.snakeToKebab("foo_bar_baz")) // => "foo-bar-baz"camelToSnake
Section titled “camelToSnake”camelCase to snake_case.
import { String } from "effect"
console.log(String.camelToSnake("helloWorld")) // => "hello_world"console.log(String.camelToSnake("fooBarBaz")) // => "foo_bar_baz"pascalToSnake
Section titled “pascalToSnake”PascalCase to snake_case.
import { String } from "effect"
console.log(String.pascalToSnake("HelloWorld")) // => "hello_world"console.log(String.pascalToSnake("FooBarBaz")) // => "foo_bar_baz"kebabToSnake
Section titled “kebabToSnake”kebab-case to snake_case (swaps - for _).
import { String } from "effect"
console.log(String.kebabToSnake("hello-world")) // => "hello_world"console.log(String.kebabToSnake("foo-bar-baz")) // => "foo_bar_baz"noCase
Section titled “noCase”The general normalizer the others are built on: splits input into word parts at
case boundaries and non-word characters, transforms each part (default
toLowerCase), and joins with a delimiter (default " "). Options:
splitRegExp, stripRegExp, delimiter, and transform.
import { String } from "effect"
console.log(String.noCase("HelloWorld")) // => "hello world"console.log(String.noCase("user_profile-ID")) // => "user profile id"
// Custom delimiter + transform → dot.caseconsole.log( String.noCase("HelloWorld", { delimiter: ".", transform: String.toLowerCase })) // => "hello.world"camelCase
Section titled “camelCase”Normalizes free-form input into lower-initial camelCase.
import { String } from "effect"
console.log(String.camelCase("Hello world")) // => "helloWorld"console.log(String.camelCase("user_profile_id")) // => "userProfileId"pascalCase
Section titled “pascalCase”Normalizes free-form input into upper-initial PascalCase.
import { String } from "effect"
console.log(String.pascalCase("hello world")) // => "HelloWorld"console.log(String.pascalCase("user-profile-id")) // => "UserProfileId"snakeCase
Section titled “snakeCase”Normalizes free-form input into lowercase snake_case.
import { String } from "effect"
console.log(String.snakeCase("Hello World")) // => "hello_world"console.log(String.snakeCase("userProfileId")) // => "user_profile_id"kebabCase
Section titled “kebabCase”Normalizes free-form input into lowercase kebab-case — ideal for slugs.
import { String } from "effect"
console.log(String.kebabCase("User profile ID")) // => "user-profile-id"console.log(String.kebabCase("helloWorld")) // => "hello-world"constantCase
Section titled “constantCase”Normalizes free-form input into uppercase, underscore-separated
CONSTANT_CASE.
import { String } from "effect"
console.log(String.constantCase("Hello World")) // => "HELLO_WORLD"console.log(String.constantCase("userProfileId")) // => "USER_PROFILE_ID"RegExp helpers
Section titled “RegExp helpers”The tiny RegExp module rounds out the string toolkit: the native constructor,
a guard, and — most usefully — escape for safely embedding untrusted literal
text inside a pattern.
RegExp
Section titled “RegExp”The native globalThis.RegExp constructor, re-exported.
import { RegExp } from "effect"
const pattern = new RegExp.RegExp("hello", "i")console.log(pattern.test("Hello World")) // => trueconsole.log(pattern.test("goodbye")) // => falseisRegExp
Section titled “isRegExp”A guard that narrows unknown input to RegExp.
import { RegExp } from "effect"
console.log(RegExp.isRegExp(/a/)) // => trueconsole.log(RegExp.isRegExp("a")) // => falseescape
Section titled “escape”Escapes regular-expression metacharacters in a string so it matches literally.
Always run user-supplied text through escape before interpolating it into a
pattern — otherwise characters like . or * are interpreted as regex syntax.
import { RegExp } from "effect"
console.log(RegExp.escape("a*b")) // => "a\\*b"
// Build a literal matcher from untrusted input safelyconst userInput = "a+b.txt"const expression = new RegExp.RegExp(`^${RegExp.escape(userInput)}$`)console.log(expression.test("a+b.txt")) // => trueconsole.log(expression.test("aaab.txt")) // => falseescape only escapes metacharacters — it does not add anchors, flags, or
grouping, so you remain in control of the surrounding pattern.