Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IsStringLiteral: Fix instantiations with infinite string types #1044

Merged
merged 6 commits into from
Jan 20, 2025

Conversation

som-sm
Copy link
Collaborator

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

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
12 checks passed
@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