Skip to content

Commit 1adbf5b

Browse files
committed
IsLiteral: Adding strict options and Fixing unpredictable behaviors of IsStringLiteral returning boolean for unions and IsNumericLiteral returning false for numeric unions
- Matching `IsStringLiteral` behavior to the other is*Literals when dealing with unions of different types return boolean, now return false. - Fixing `IsNumericLiteral` return false for numeric union like (1 | 1n), now return true. - Adding `strict` option to control the behavior of `IsStringLiteral` against infinite signature types.
1 parent eb37799 commit 1adbf5b

File tree

3 files changed

+204
-98
lines changed

3 files changed

+204
-98
lines changed

source/internal/type.d.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import type {If} from '../if.js';
2-
import type {IsAny} from '../is-any.d.ts';
3-
import type {IsNever} from '../is-never.d.ts';
41
import type {Primitive} from '../primitive.d.ts';
2+
import type {IsNever} from '../is-never.d.ts';
3+
import type {IsAny} from '../is-any.d.ts';
4+
import type {And} from '../and.js';
5+
import type {If} from '../if.js';
56

67
/**
78
Matches any primitive, `void`, `Date`, or `RegExp` value.
@@ -13,14 +14,20 @@ Matches non-recursive types.
1314
*/
1415
export type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown);
1516

17+
/**
18+
* Checks if one type extends another. Note: this is not quite the same as `Left extends Right` because:
19+
* 1. If either type is `never`, the result is `true` iff the other type is also `never`.
20+
* 2. Types are wrapped in a 1-tuple so that union types are not distributed - instead we consider `string | number` to _not_ extend `number`. If we used `Left extends Right` directly you would get `Extends<string | number, number>` => `false | true` => `boolean`.
21+
*/
22+
export type Extends<Left, Right> = IsNever<Left> extends true ? IsNever<Right> : [Left] extends [Right] ? true : false;
23+
1624
/**
1725
Returns a boolean for whether the two given types extends the base type.
1826
*/
19-
export type IsBothExtends<BaseType, FirstType, SecondType> = FirstType extends BaseType
20-
? SecondType extends BaseType
21-
? true
22-
: false
23-
: false;
27+
export type IsBothExtends<BaseType, FirstType, SecondType> = And<
28+
Extends<FirstType, BaseType>,
29+
Extends<SecondType, BaseType>
30+
>;
2431

2532
/**
2633
Test if the given function has multiple call signatures.
@@ -40,9 +47,19 @@ export type HasMultipleCallSignatures<T extends (...arguments_: any[]) => unknow
4047
: false;
4148

4249
/**
43-
Returns a boolean for whether the given `boolean` is not `false`.
50+
Returns a boolean for whether the given `boolean` Union containe's `false`.
51+
*/
52+
export type IsNotFalse<T extends boolean> = Not<IsFalse<T>>;
53+
54+
/**
55+
Returns a boolean for whether the given `boolean` Union members are all `true`.
56+
*/
57+
export type IsTrue<T extends boolean> = Extends<T, true>;
58+
59+
/**
60+
Returns a boolean for whether the given `boolean` Union members are all `false`.
4461
*/
45-
export type IsNotFalse<T extends boolean> = [T] extends [false] ? false : true;
62+
export type IsFalse<T extends boolean> = Extends<T, false>;
4663

4764
/**
4865
Returns a boolean for whether the given type is primitive value or primitive type.
@@ -59,7 +76,7 @@ IsPrimitive<Object>
5976
//=> false
6077
```
6178
*/
62-
export type IsPrimitive<T> = [T] extends [Primitive] ? true : false;
79+
export type IsPrimitive<T> = Extends<T, Primitive>;
6380

6481
/**
6582
Returns a boolean for whether A is false.

source/is-literal.d.ts

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1+
import type {ApplyDefaultOptions, CollapseLiterals} from './internal/object.d.ts';
2+
import type {Extends, IsNotFalse, IsTrue, Not} from './internal/type.d.ts';
3+
import type {TagContainer, UnwrapTagged} from './tagged.js';
14
import type {Primitive} from './primitive.d.ts';
2-
import type {Numeric} from './numeric.d.ts';
3-
import type {CollapseLiterals, IfNotAnyOrNever, IsNotFalse, IsPrimitive} from './internal/index.d.ts';
45
import type {IsNever} from './is-never.d.ts';
5-
import type {TagContainer, UnwrapTagged} from './tagged.js';
6+
import type {Numeric} from './numeric.d.ts';
7+
import type {IsAny} from './is-any.js';
8+
import type {And} from './and.js';
9+
10+
/**
11+
@see {@link IsLiteral}
12+
*/
13+
type IsLiteralOptions = {
14+
strict?: boolean
15+
}
16+
17+
type DefaultIsLiteralOptions = {
18+
strict: true
19+
}
620

721
/**
822
Returns a boolean for whether the given type `T` is the specified `LiteralType`.
@@ -24,11 +38,10 @@ LiteralCheck<1, string>
2438
type LiteralCheck<T, LiteralType extends Primitive> = (
2539
IsNever<T> extends false // Must be wider than `never`
2640
? [T] extends [LiteralType & infer U] // Remove any branding
27-
? [U] extends [LiteralType] // Must be narrower than `LiteralType`
28-
? [LiteralType] extends [U] // Cannot be wider than `LiteralType`
29-
? false
30-
: true
31-
: false
41+
? And<
42+
Extends<U, LiteralType>, // Must be narrower than `LiteralType`
43+
Not<Extends<LiteralType, U>> // Cannot be wider than `LiteralType`
44+
>
3245
: false
3346
: false
3447
);
@@ -52,9 +65,10 @@ type LiteralChecks<T, LiteralUnionType> = (
5265
// Conditional type to force union distribution.
5366
// If `T` is none of the literal types in the union `LiteralUnionType`, then `LiteralCheck<T, LiteralType>` will evaluate to `false` for the whole union.
5467
// If `T` is one of the literal types in the union, it will evaluate to `boolean` (i.e. `true | false`)
55-
IsNotFalse<LiteralUnionType extends Primitive
56-
? LiteralCheck<T, LiteralUnionType>
57-
: never
68+
IsNotFalse<
69+
LiteralUnionType extends Primitive
70+
? LiteralCheck<T, LiteralUnionType>
71+
: never
5872
>
5973
);
6074

@@ -111,21 +125,30 @@ type L2 = Length<`${number}`>;
111125
//=> number
112126
```
113127
128+
@see IsStringPrimitive
114129
@category Type Guard
115130
@category Utilities
116131
*/
117-
export type IsStringLiteral<S> = IfNotAnyOrNever<S,
118-
_IsStringLiteral<CollapseLiterals<S extends TagContainer<any> ? UnwrapTagged<S> : S>>,
119-
false, false>;
120-
121-
export type _IsStringLiteral<S> =
122-
// If `T` is an infinite string type (e.g., `on${string}`), `Record<T, never>` produces an index signature,
123-
// and since `{}` extends index signatures, the result becomes `false`.
124-
S extends string
125-
? {} extends Record<S, never>
126-
? false
127-
: true
128-
: false;
132+
export type IsStringLiteral<T, Options extends IsLiteralOptions = {}> =
133+
ApplyDefaultOptions<IsLiteralOptions, DefaultIsLiteralOptions, Options> extends infer ResolvedOptions extends Required<IsLiteralOptions>
134+
? IsNever<T> extends false
135+
? CollapseLiterals<T extends TagContainer<any> ? UnwrapTagged<T> : T> extends infer Type
136+
? ResolvedOptions['strict'] extends true
137+
? IsTrue<_IsStringLiteral<Type>>
138+
: LiteralCheck<Type, string>
139+
: never
140+
: false
141+
: never
142+
143+
type _IsStringLiteral<S> = (
144+
// If `T` is an infinite string type (e.g., `on${string}`), `Record<T, never>` produces an index signature,
145+
// and since `{}` extends index signatures, the result becomes `false`.
146+
S extends string
147+
? {} extends Record<S, never>
148+
? false
149+
: true
150+
: false
151+
);
129152

130153
/**
131154
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).
@@ -170,10 +193,19 @@ endsWith('abc123', end);
170193
//=> boolean
171194
```
172195
196+
@see IsNumericPrimitive
173197
@category Type Guard
174198
@category Utilities
175199
*/
176-
export type IsNumericLiteral<T> = LiteralChecks<T, Numeric>;
200+
export type IsNumericLiteral<T> = (
201+
IsAny<T> extends false
202+
? T extends number
203+
? T extends bigint
204+
? LiteralCheck<T, Numeric>
205+
: LiteralChecks<T, Numeric>
206+
: LiteralChecks<T, Numeric>
207+
: false
208+
)
177209

178210
/**
179211
Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
@@ -210,6 +242,7 @@ const eitherId = getId({asString: runtimeBoolean});
210242
//=> number | string
211243
```
212244
245+
@see IsBooleanPrimitive
213246
@category Type Guard
214247
@category Utilities
215248
*/
@@ -245,14 +278,15 @@ get({[symbolValue]: 1} as const, symbolValue);
245278
//=> number
246279
```
247280
281+
@see IsSymbolPrimitive
248282
@category Type Guard
249283
@category Utilities
250284
*/
251285
export type IsSymbolLiteral<T> = LiteralCheck<T, symbol>;
252286

253287
/** Helper type for `IsLiteral`. */
254-
type IsLiteralUnion<T> =
255-
| IsStringLiteral<T>
288+
type IsLiteralUnion<T, O extends IsLiteralOptions> =
289+
| IsStringLiteral<T, O>
256290
| IsNumericLiteral<T>
257291
| IsBooleanLiteral<T>
258292
| IsSymbolLiteral<T>;
@@ -270,13 +304,13 @@ import type {IsLiteral} from 'type-fest';
270304
271305
// https://github.com/inocan-group/inferred-types/blob/master/src/types/string-literals/StripLeading.ts
272306
export type StripLeading<A, B> =
273-
A extends string
274-
? B extends string
275-
? IsLiteral<A> extends true
276-
? string extends B ? never : A extends `${B & string}${infer After}` ? After : A
277-
: string
278-
: A
279-
: A;
307+
A extends string
308+
? B extends string
309+
? IsLiteral<A> extends true
310+
? string extends B ? never : A extends `${B & string}${infer After}` ? After : A
311+
: string
312+
: A
313+
: A;
280314
281315
function stripLeading<Input extends string, Strip extends string>(input: Input, strip: Strip) {
282316
return input.replace(`^${strip}`, '') as StripLeading<Input, Strip>;
@@ -291,10 +325,12 @@ stripLeading(str, 'abc');
291325
//=> string
292326
```
293327
328+
@see IsPrimitive
294329
@category Type Guard
295330
@category Utilities
296331
*/
297-
export type IsLiteral<T> =
298-
IsPrimitive<T> extends true
299-
? IsNotFalse<IsLiteralUnion<T>>
300-
: false;
332+
export type IsLiteral<T, Options extends IsLiteralOptions = {}> = (
333+
Extends<T, Primitive> extends true
334+
? IsNotFalse<IsLiteralUnion<T, Options>>
335+
: false
336+
);

0 commit comments

Comments
 (0)