Skip to content

IsStringLiteral: Fix instantiations with infinite string types - #1044

Merged
sindresorhus merged 6 commits into
mainfrom
fix/is-string-literal-with-infinite-string-types
Jan 20, 2025
Merged

sindresorhus merged 6 commits into
mainfrom
fix/is-string-literal-with-infinite-string-types

Conversation

@som-sm

@som-sm som-sm commented Jan 16, 2025 •

Copy link
Copy Markdown
Collaborator

Fixes #1032

Found a really nice trick to solve this! Refer to the code comments explaining how it works:

export type IsStringLiteral<T> = T extends string
// If `T` is an infinite string type (e.g., `on${string}`), `Record<T, never>` produces an index signature,
// and since `{}` extends index signatures, the result becomes `false`.
? {} extends Record<T, never>
? false
: true
: false;


This fix will make this type quite useful because almost all string utilities will require an IsStringLiteral check. For example:

import { Split, Words, StringSlice } from "type-fest";

type T1 = Split<Uppercase<string>, "">;
// Actual: [Uppercase<string>]
// Ideal: string[]

type T2 = Split<`${string}-${string}`, "">;
// Actual: [string, "-", string]
// Ideal: string[]

type T3 = Words<Uppercase<string>>;
// Actual: []
// Ideal: string[]

type T4 = Words<`${string}-${string}`>;
// Actual: [string]
// Ideal: string[]

type T5 = StringSlice<Uppercase<string>>;
// Actual: ""
// Ideal: string[]

I'll open up a separate PR fixing all the above mentioned cases.

I've mentioned the same as an example in the documentation:

// This behaviour is particularly useful in string manipulation utilities, as infinite string types often require separate handling.
type Length<S extends string, Counter extends never[] = []> =
IsStringLiteral<S> extends false
? number // return `number` for infinite string types
: S extends `${any}${infer Tail}`
? Length<Tail, [...Counter, never]>
: Counter['length'];
type L1 = Length<Lowercase<string>>;
//=> number
type L2 = Length<`${number}`>;
//=> number


NOTE: If IsStringLiteral is instantiated with a union of literal strings and non-string types, it currently returns false because not all union members are literal strings. However, after this PR, cases like these would instead return boolean.

type T1 = IsStringLiteral<null | "a">
//   ^? type T6 = false // Current behaviour

type T2 = IsStringLiteral<null | "a">
//   ^? type T6 = boolean // Updated behaviour

To me, the current behaviour seems more like a bug than an intentional choice, as returning boolean makes more sense and allows users to decide how to handle mixed union cases.

@som-sm
som-sm force-pushed the fix/is-string-literal-with-infinite-string-types branch 3 times, most recently from 6467614 to 1edeaf9 Compare January 16, 2025 10:53
@som-sm
som-sm marked this pull request as ready for review January 16, 2025 10:55
@som-sm
som-sm force-pushed the fix/is-string-literal-with-infinite-string-types branch 2 times, most recently from 9842e33 to 9c842e5 Compare January 16, 2025 11:10
@som-sm
som-sm force-pushed the fix/is-string-literal-with-infinite-string-types branch from 9c842e5 to 0ca76a9 Compare January 16, 2025 11:12
@sindresorhus
sindresorhus merged commit e7800af into main Jan 20, 2025
@sindresorhus
sindresorhus deleted the fix/is-string-literal-with-infinite-string-types branch January 20, 2025 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IsStringLiteral<T> returns true for string types that have an unconstrained string component

2 participants