Skip to content

Commit

Permalink
IsStringLiteral: Fix instantiations with infinite string types (#1044)
Browse files Browse the repository at this point in the history
  • Loading branch information
som-sm authored Jan 20, 2025
1 parent 49605b9 commit e7800af
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 1 deletion.
40 changes: 39 additions & 1 deletion source/is-literal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {Primitive} from './primitive';
import type {Numeric} from './numeric';
import type {IsNotFalse, IsPrimitive} from './internal';
import type {IsNever} from './is-never';
import type {IfNever} from './if-never';

/**
Returns a boolean for whether the given type `T` is the specified `LiteralType`.
Expand Down Expand Up @@ -65,6 +66,8 @@ Useful for:
- constraining strings to be a string literal
- type utilities, such as when constructing parsers and ASTs
The implementation of this type is inspired by the trick mentioned in this [StackOverflow answer](https://stackoverflow.com/a/68261113/420747).
@example
```
import type {IsStringLiteral} from 'type-fest';
Expand All @@ -80,10 +83,45 @@ const output = capitalize('hello, world!');
//=> 'Hello, world!'
```
@example
```
// String types with infinite set of possible values return `false`.
import type {IsStringLiteral} from 'type-fest';
type AllUppercaseStrings = IsStringLiteral<Uppercase<string>>;
//=> false
type StringsStartingWithOn = IsStringLiteral<`on${string}`>;
//=> false
// 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 `${string}${infer Tail}`
? Length<Tail, [...Counter, never]>
: Counter['length'];
type L1 = Length<Lowercase<string>>;
//=> number
type L2 = Length<`${number}`>;
//=> number
```
@category Type Guard
@category Utilities
*/
export type IsStringLiteral<T> = LiteralCheck<T, string>;
export type IsStringLiteral<T> = IfNever<T, false,
// 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`.
T extends string
? {} extends Record<T, never>
? false
: true
: false>;

/**
Returns a boolean for whether the given type is a `number` or `bigint` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
Expand Down
39 changes: 39 additions & 0 deletions test-d/is-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,45 @@ expectType<IsLiteral<never>>(false);
expectType<IsStringLiteral<typeof stringLiteral>>(true);
expectType<IsStringLiteral<typeof _string>>(false);

// Strings with infinite set of possible values return `false`
expectType<IsStringLiteral<Uppercase<string>>>(false);
expectType<IsStringLiteral<Lowercase<string>>>(false);
expectType<IsStringLiteral<Capitalize<string>>>(false);
expectType<IsStringLiteral<Uncapitalize<string>>>(false);
expectType<IsStringLiteral<Capitalize<Lowercase<string>>>>(false);
expectType<IsStringLiteral<Uncapitalize<Uppercase<string>>>>(false);
expectType<IsStringLiteral<`abc${string}`>>(false);
expectType<IsStringLiteral<`${string}abc`>>(false);
expectType<IsStringLiteral<`${number}:${string}`>>(false);
expectType<IsStringLiteral<`abc${Uppercase<string>}`>>(false);
expectType<IsStringLiteral<`${Lowercase<string>}abc`>>(false);
expectType<IsStringLiteral<`${number}`>>(false);
expectType<IsStringLiteral<`${number}${string}`>>(false);
expectType<IsStringLiteral<`${number}` | Uppercase<string>>>(false);
expectType<IsStringLiteral<Capitalize<string> | Uppercase<string>>>(false);
expectType<IsStringLiteral<`abc${string}` | `${string}abc`>>(false);

// Strings with finite set of possible values return `true`
expectType<IsStringLiteral<'a' | 'b'>>(true);
expectType<IsStringLiteral<Uppercase<'a'>>>(true);
expectType<IsStringLiteral<Lowercase<'a'>>>(true);
expectType<IsStringLiteral<Uppercase<'a' | 'b'>>>(true);
expectType<IsStringLiteral<Lowercase<'a' | 'b'>>>(true);
expectType<IsStringLiteral<Capitalize<'abc' | 'xyz'>>>(true);
expectType<IsStringLiteral<Uncapitalize<'Abc' | 'Xyz'>>>(true);
expectType<IsStringLiteral<`ab${'c' | 'd' | 'e'}`>>(true);
expectType<IsStringLiteral<Uppercase<'a' | 'b'> | 'C' | 'D'>>(true);
expectType<IsStringLiteral<Lowercase<'xyz'> | Capitalize<'abc'>>>(true);

// Strings with union of literals and non-literals return `boolean`
expectType<IsStringLiteral<Uppercase<string> | 'abc'>>({} as boolean);
expectType<IsStringLiteral<Lowercase<string> | 'Abc'>>({} as boolean);
expectType<IsStringLiteral<null | '1' | '2' | '3'>>({} as boolean);

// Boundary types
expectType<IsStringLiteral<any>>(false);
expectType<IsStringLiteral<never>>(false);

expectType<IsNumericLiteral<typeof numberLiteral>>(true);
expectType<IsNumericLiteral<typeof bigintLiteral>>(true);
expectType<IsNumericLiteral<typeof _number>>(false);
Expand Down

0 comments on commit e7800af

Please sign in to comment.