Skip to content

Commit e7800af

Browse files
authored
IsStringLiteral: Fix instantiations with infinite string types (#1044)
1 parent 49605b9 commit e7800af

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

source/is-literal.d.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {Primitive} from './primitive';
22
import type {Numeric} from './numeric';
33
import type {IsNotFalse, IsPrimitive} from './internal';
44
import type {IsNever} from './is-never';
5+
import type {IfNever} from './if-never';
56

67
/**
78
Returns a boolean for whether the given type `T` is the specified `LiteralType`.
@@ -65,6 +66,8 @@ Useful for:
6566
- constraining strings to be a string literal
6667
- type utilities, such as when constructing parsers and ASTs
6768
69+
The implementation of this type is inspired by the trick mentioned in this [StackOverflow answer](https://stackoverflow.com/a/68261113/420747).
70+
6871
@example
6972
```
7073
import type {IsStringLiteral} from 'type-fest';
@@ -80,10 +83,45 @@ const output = capitalize('hello, world!');
8083
//=> 'Hello, world!'
8184
```
8285
86+
@example
87+
```
88+
// String types with infinite set of possible values return `false`.
89+
90+
import type {IsStringLiteral} from 'type-fest';
91+
92+
type AllUppercaseStrings = IsStringLiteral<Uppercase<string>>;
93+
//=> false
94+
95+
type StringsStartingWithOn = IsStringLiteral<`on${string}`>;
96+
//=> false
97+
98+
// This behaviour is particularly useful in string manipulation utilities, as infinite string types often require separate handling.
99+
100+
type Length<S extends string, Counter extends never[] = []> =
101+
IsStringLiteral<S> extends false
102+
? number // return `number` for infinite string types
103+
: S extends `${string}${infer Tail}`
104+
? Length<Tail, [...Counter, never]>
105+
: Counter['length'];
106+
107+
type L1 = Length<Lowercase<string>>;
108+
//=> number
109+
110+
type L2 = Length<`${number}`>;
111+
//=> number
112+
```
113+
83114
@category Type Guard
84115
@category Utilities
85116
*/
86-
export type IsStringLiteral<T> = LiteralCheck<T, string>;
117+
export type IsStringLiteral<T> = IfNever<T, false,
118+
// If `T` is an infinite string type (e.g., `on${string}`), `Record<T, never>` produces an index signature,
119+
// and since `{}` extends index signatures, the result becomes `false`.
120+
T extends string
121+
? {} extends Record<T, never>
122+
? false
123+
: true
124+
: false>;
87125

88126
/**
89127
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).

test-d/is-literal.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,45 @@ expectType<IsLiteral<never>>(false);
4444
expectType<IsStringLiteral<typeof stringLiteral>>(true);
4545
expectType<IsStringLiteral<typeof _string>>(false);
4646

47+
// Strings with infinite set of possible values return `false`
48+
expectType<IsStringLiteral<Uppercase<string>>>(false);
49+
expectType<IsStringLiteral<Lowercase<string>>>(false);
50+
expectType<IsStringLiteral<Capitalize<string>>>(false);
51+
expectType<IsStringLiteral<Uncapitalize<string>>>(false);
52+
expectType<IsStringLiteral<Capitalize<Lowercase<string>>>>(false);
53+
expectType<IsStringLiteral<Uncapitalize<Uppercase<string>>>>(false);
54+
expectType<IsStringLiteral<`abc${string}`>>(false);
55+
expectType<IsStringLiteral<`${string}abc`>>(false);
56+
expectType<IsStringLiteral<`${number}:${string}`>>(false);
57+
expectType<IsStringLiteral<`abc${Uppercase<string>}`>>(false);
58+
expectType<IsStringLiteral<`${Lowercase<string>}abc`>>(false);
59+
expectType<IsStringLiteral<`${number}`>>(false);
60+
expectType<IsStringLiteral<`${number}${string}`>>(false);
61+
expectType<IsStringLiteral<`${number}` | Uppercase<string>>>(false);
62+
expectType<IsStringLiteral<Capitalize<string> | Uppercase<string>>>(false);
63+
expectType<IsStringLiteral<`abc${string}` | `${string}abc`>>(false);
64+
65+
// Strings with finite set of possible values return `true`
66+
expectType<IsStringLiteral<'a' | 'b'>>(true);
67+
expectType<IsStringLiteral<Uppercase<'a'>>>(true);
68+
expectType<IsStringLiteral<Lowercase<'a'>>>(true);
69+
expectType<IsStringLiteral<Uppercase<'a' | 'b'>>>(true);
70+
expectType<IsStringLiteral<Lowercase<'a' | 'b'>>>(true);
71+
expectType<IsStringLiteral<Capitalize<'abc' | 'xyz'>>>(true);
72+
expectType<IsStringLiteral<Uncapitalize<'Abc' | 'Xyz'>>>(true);
73+
expectType<IsStringLiteral<`ab${'c' | 'd' | 'e'}`>>(true);
74+
expectType<IsStringLiteral<Uppercase<'a' | 'b'> | 'C' | 'D'>>(true);
75+
expectType<IsStringLiteral<Lowercase<'xyz'> | Capitalize<'abc'>>>(true);
76+
77+
// Strings with union of literals and non-literals return `boolean`
78+
expectType<IsStringLiteral<Uppercase<string> | 'abc'>>({} as boolean);
79+
expectType<IsStringLiteral<Lowercase<string> | 'Abc'>>({} as boolean);
80+
expectType<IsStringLiteral<null | '1' | '2' | '3'>>({} as boolean);
81+
82+
// Boundary types
83+
expectType<IsStringLiteral<any>>(false);
84+
expectType<IsStringLiteral<never>>(false);
85+
4786
expectType<IsNumericLiteral<typeof numberLiteral>>(true);
4887
expectType<IsNumericLiteral<typeof bigintLiteral>>(true);
4988
expectType<IsNumericLiteral<typeof _number>>(false);

0 commit comments

Comments
 (0)