Skip to content

String

The string category identifies string, string literals and template literals, and provides a few type-level string operations. The predicates follow the usual type branching contract, so each one can act as a boolean check, as a filter, or as a branch selector for your own types.

type IsString<T, $O extends $StrictOptions<$O, IsString.$Options> = {}>
type IsNotString<T, $O extends $StrictOptions<$O, IsNotString.$Options> = {}>

IsString<T> is true for string and for any string literal.

type R1 = IsString<string> // true
type R2 = IsString<'a'> // true
type R3 = IsString<1> // false
type R4 = IsString<string | boolean> // boolean

$Options combines $Selection.Options, $Distributive.Options, $Exact.Options and input options for any, unknown, never and void. That means selection: 'filter' returns the input instead of a boolean, and distributive: false stops a union from being evaluated member by member:

type R1 = IsString<'a', { selection: 'filter' }> // 'a'
type R2 = IsString<string | boolean, { selection: 'filter' }> // string
type R3 = IsString<string | 1, { distributive: false }> // false

IsNotString<T> is the negation, and its filter selection keeps everything that is not a string:

type R1 = IsNotString<1> // true
type R2 = IsNotString<'a'> // false

See Options for what selection, distributive and exact mean, and type branching for $any, $unknown, $never, $void, $then, $else and the IsString.$Branch selectors.

type IsStringLiteral<T, $O extends $StrictOptions<$O, IsStringLiteral.$Options> = {}>
type IsNotStringLiteral<T, $O extends $StrictOptions<$O, IsNotStringLiteral.$Options> = {}>

Distinguishes a literal from the wide string type. Template literals count as literals by default.

type R1 = IsStringLiteral<'a'> // true
type R2 = IsStringLiteral<`${number}`> // true
type R3 = IsStringLiteral<string> // false

Use exact: true to reject template literals and accept only plain literals:

type R1 = IsStringLiteral<`${number}`> // true
type R2 = IsStringLiteral<`${number}`, { exact: true }> // false

IsTemplateLiteral and IsNotTemplateLiteral

Section titled “IsTemplateLiteral and IsNotTemplateLiteral”
type IsTemplateLiteral<T, $O extends $StrictOptions<$O, IsTemplateLiteral.$Options> = {}>
type IsNotTemplateLiteral<T, $O extends $StrictOptions<$O, IsNotTemplateLiteral.$Options> = {}>

The mirror image of the above: only template literals pass.

type R1 = IsTemplateLiteral<`a${number}`> // true
type R2 = IsTemplateLiteral<'foo'> // false
type R3 = IsTemplateLiteral<string> // false
type R4 = IsTemplateLiteral<`${number}`, { selection: 'filter' }> // `${number}`
type R5 = IsTemplateLiteral<'a', { selection: 'filter' }> // never
type StringIncludes<Subject extends string, Search extends string, Then = true, Else = false>

Checks whether Subject contains Search. This type predates type branching and takes plain Then/Else parameters instead of an options object.

type R1 = StringIncludes<'abc', 'a'> // true
type R2 = StringIncludes<'abc', 'd'> // false
type R3 = StringIncludes<'abc', 'd', 'yes', 'no'> // 'no'
type StringSplit<Subject extends string, Separator extends string>

Splits Subject on Separator and returns a tuple. An empty separator splits into characters.

type R1 = StringSplit<'abc', ''> // ['a', 'b', 'c']
type R2 = StringSplit<'a.b.c', '.'> // ['a', 'b', 'c']
type R3 = StringSplit<'abc', 'b'> // ['a', 'c']

StringPlus holds the type-level forms of the String.prototype methods, under names too generic for the top level. Includes and Split are the same operations as StringIncludes and StringSplit, under shorter names.

import type { StringPlus } from 'type-plus'
type R1 = StringPlus.Includes<'abc', 'a'> // true
type R2 = StringPlus.Split<'abc', ''> // ['a', 'b', 'c']

StringPlus.Includes takes the type branching options object rather than the positional Then/Else that StringIncludes still uses.

type R = StringPlus.Includes<'abc', 'd', { $then: 'yes'; $else: 'no' }> // 'no'
type R = StringPlus.Includes<'abc', 'a', { selection: 'filter' }> // 'abc'
type R = StringPlus.Includes<'abc', 'd', { selection: 'filter' }> // never
namespace StringPlus {
type StartsWith<Subject extends string, Search extends string, $O extends $StrictOptions<$O, StartsWith.$Options> = {}>
type EndsWith<Subject extends string, Search extends string, $O extends $StrictOptions<$O, EndsWith.$Options> = {}>
}

The type-level String.prototype.startsWith and endsWith. They are predicates shaped like StringPlus.Includes: an empty Search matches every string literal, and the wide string takes the $else branch.

type R = StringPlus.StartsWith<'abc', 'ab'> // true
type R = StringPlus.StartsWith<'abc', 'bc'> // false
type R = StringPlus.EndsWith<'abc', 'bc'> // true
type R = StringPlus.EndsWith<'abc', 'ab'> // false
type R = StringPlus.StartsWith<'abc' | 'bcd', 'ab', { selection: 'filter' }> // 'abc'
type R = StringPlus.EndsWith<'abc', 'ab', { $then: 'yes'; $else: 'no' }> // 'no'
namespace StringPlus {
type Replace<Subject extends string, Search extends string, Replacement extends string>
type ReplaceAll<Subject extends string, Search extends string, Replacement extends string>
}

The type-level String.prototype.replace and replaceAll with a string pattern. Replace replaces the first occurrence of Search, and ReplaceAll replaces every occurrence, left to right and without overlap. A Subject without Search comes back unchanged. A wide Subject or Search gives string.

type R = StringPlus.Replace<'a.b.c', '.', '/'> // 'a/b.c'
type R = StringPlus.ReplaceAll<'a.b.c', '.', '/'> // 'a/b/c'
type R = StringPlus.ReplaceAll<'aaa', 'aa', 'b'> // 'ba'
type R = StringPlus.Replace<'abc', 'd', 'x'> // 'abc'

An empty Search behaves as it does at runtime:

type R = StringPlus.Replace<'abc', '', 'x'> // 'xabc'
type R = StringPlus.ReplaceAll<'abc', '', '-'> // '-a-b-c-'

The inverse of StringPlus.Split is ArrayPlus.Join, because join is an Array.prototype method.

type R = ArrayPlus.Join<StringPlus.Split<'a.b.c', '.'>, '.'> // 'a.b.c'
type ExtractManipulatedString<T extends string>

A type util. It unwraps the intrinsic string manipulation types — Uppercase, Lowercase, Capitalize and Uncapitalize — to recover the string being manipulated. IsStringLiteral uses it to see through those wrappers.

It only sees a wrapper that TypeScript has not already resolved. Applied to a literal, the intrinsic evaluates first and there is nothing left to unwrap:

type R1 = ExtractManipulatedString<Uppercase<string>> // string
type R2 = ExtractManipulatedString<Uppercase<'abc'>> // 'ABC'
type R3 = ExtractManipulatedString<'abc'> // 'abc'
Type Description
IsString<T, $O> T is string or a string literal
IsNotString<T, $O> T is neither string nor a string literal
IsStringLiteral<T, $O> T is a string literal (template literals included unless exact: true)
IsNotStringLiteral<T, $O> T is not a string literal
IsTemplateLiteral<T, $O> T is a template literal
IsNotTemplateLiteral<T, $O> T is not a template literal
StringIncludes<S, Search, Then, Else> S contains Search
StringSplit<S, Separator> split S into a tuple
StringPlus.Includes<S, Search, $O> / StringPlus.Split<S, Separator> namespaced aliases of the two above; Includes takes $O
StringPlus.StartsWith<S, Search, $O> S starts with Search
StringPlus.EndsWith<S, Search, $O> S ends with Search
StringPlus.Replace<S, Search, Replacement> replace the first Search in S
StringPlus.ReplaceAll<S, Search, Replacement> replace every Search in S
ExtractManipulatedString<T> unwrap Uppercase/Lowercase/Capitalize/Uncapitalize

Source: src/string.