Skip to content

Commit 9d00bac

Browse files
authoredMar 10, 2023
Add IsLiteral types (#563)
1 parent a065865 commit 9d00bac

File tree

6 files changed

+342
-1
lines changed

6 files changed

+342
-1
lines changed
 

‎index.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ export type {HasRequiredKeys} from './source/has-required-keys';
7676
export type {Spread} from './source/spread';
7777
export type {TupleToUnion} from './source/tuple-to-union';
7878
export type {IsEqual} from './source/is-equal';
79+
export type {
80+
IsLiteral,
81+
IsStringLiteral,
82+
IsNumericLiteral,
83+
IsBooleanLiteral,
84+
IsSymbolLiteral,
85+
} from './source/is-literal';
7986

8087
// Template literal types
8188
export type {CamelCase} from './source/camel-case';

‎readme.md

+5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ Click the type names for complete docs.
171171
- [`HasRequiredKeys`](source/has-required-keys.d.ts) - Create a `true`/`false` type depending on whether the given type has any required fields.
172172
- [`Spread`](source/spread.d.ts) - Mimic the type inferred by TypeScript when merging two objects or two arrays/tuples using the spread syntax.
173173
- [`IsEqual`](source/is-equal.d.ts) - Returns a boolean for whether the two given types are equal.
174+
- [`IsLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
175+
- [`IsStringLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `string` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
176+
- [`IsNumericLiteral`](source/is-literal.d.ts) - 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).
177+
- [`IsBooleanLiteral`](source/is-literal.d.ts) - 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).
178+
- [`IsSymbolLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
174179

175180
### JSON
176181

‎source/internal.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,8 @@ export type HasMultipleCallSignatures<T extends (...arguments: any[]) => unknown
257257
? false
258258
: true
259259
: false;
260+
261+
/**
262+
Returns a boolean for whether the given `boolean` is not `false`.
263+
*/
264+
export type IsNotFalse<T extends boolean> = [T] extends [false] ? false : true;

‎source/is-literal.d.ts

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import type {Primitive} from './primitive';
2+
import type {Numeric} from './numeric';
3+
import type {IsNever, IsNotFalse} from './internal';
4+
5+
/**
6+
Returns a boolean for whether the given type `T` is the specified `LiteralType`.
7+
8+
@link https://stackoverflow.com/a/52806744/10292952
9+
10+
@example
11+
```
12+
LiteralCheck<1, number>
13+
//=> true
14+
15+
LiteralCheck<number, number>
16+
//=> false
17+
18+
LiteralCheck<1, string>
19+
//=> false
20+
```
21+
*/
22+
type LiteralCheck<T, LiteralType extends Primitive> = (
23+
IsNever<T> extends false // Must be wider than `never`
24+
? [T] extends [LiteralType] // Must be narrower than `LiteralType`
25+
? [LiteralType] extends [T] // Cannot be wider than `LiteralType`
26+
? false
27+
: true
28+
: false
29+
: false
30+
);
31+
32+
/**
33+
Returns a boolean for whether the given type `T` is one of the specified literal types in `LiteralUnionType`.
34+
35+
@example
36+
```
37+
LiteralChecks<1, Numeric>
38+
//=> true
39+
40+
LiteralChecks<1n, Numeric>
41+
//=> true
42+
43+
LiteralChecks<bigint, Numeric>
44+
//=> false
45+
```
46+
*/
47+
type LiteralChecks<T, LiteralUnionType> = (
48+
// Conditional type to force union distribution.
49+
// If `T` is none of the literal types in the union `LiteralUnionType`, then `LiteralCheck<T, LiteralType>` will evaluate to `false` for the whole union.
50+
// If `T` is one of the literal types in the union, it will evaluate to `boolean` (i.e. `true | false`)
51+
IsNotFalse<LiteralUnionType extends Primitive
52+
? LiteralCheck<T, LiteralUnionType>
53+
: never
54+
>
55+
);
56+
57+
/**
58+
Returns a boolean for whether the given type is a `string` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
59+
60+
Useful for:
61+
- providing strongly-typed string manipulation functions
62+
- constraining strings to be a string literal
63+
- type utilities, such as when constructing parsers and ASTs
64+
65+
@example
66+
```
67+
import type {IsStringLiteral} from 'type-fest';
68+
69+
type CapitalizedString<T extends string> = IsStringLiteral<T> extends true ? Capitalize<T> : string;
70+
71+
// https://github.com/yankeeinlondon/native-dash/blob/master/src/capitalize.ts
72+
function capitalize<T extends Readonly<string>>(input: T): CapitalizedString<T> {
73+
return (input.slice(0, 1).toUpperCase() + input.slice(1)) as CapitalizedString<T>;
74+
}
75+
76+
const output = capitalize('hello, world!');
77+
//=> 'Hello, world!'
78+
```
79+
80+
@category Utilities
81+
@category Type Guard
82+
*/
83+
export type IsStringLiteral<T> = LiteralCheck<T, string>;
84+
85+
/**
86+
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).
87+
88+
Useful for:
89+
- providing strongly-typed functions when given literal arguments
90+
- type utilities, such as when constructing parsers and ASTs
91+
92+
@example
93+
```
94+
import type {IsNumericLiteral} from 'type-fest';
95+
96+
// https://github.com/inocan-group/inferred-types/blob/master/src/types/boolean-logic/EndsWith.ts
97+
type EndsWith<TValue, TEndsWith extends string> =
98+
TValue extends string
99+
? IsStringLiteral<TEndsWith> extends true
100+
? IsStringLiteral<TValue> extends true
101+
? TValue extends `${string}${TEndsWith}`
102+
? true
103+
: false
104+
: boolean
105+
: boolean
106+
: TValue extends number
107+
? IsNumericLiteral<TValue> extends true
108+
? EndsWith<`${TValue}`, TEndsWith>
109+
: false
110+
: false;
111+
112+
function endsWith<Input extends string | number, End extends string>(input: Input, end: End) {
113+
return `${input}`.endsWith(end) as EndsWith<Input, End>;
114+
}
115+
116+
endsWith('abc', 'c');
117+
//=> true
118+
119+
endsWith(123456, '456');
120+
//=> true
121+
122+
const end = '123' as string;
123+
124+
endsWith('abc123', end);
125+
//=> boolean
126+
```
127+
128+
@category Utilities
129+
@category Type Guard
130+
*/
131+
export type IsNumericLiteral<T> = LiteralChecks<T, Numeric>;
132+
133+
/**
134+
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).
135+
136+
Useful for:
137+
- providing strongly-typed functions when given literal arguments
138+
- type utilities, such as when constructing parsers and ASTs
139+
140+
@example
141+
```
142+
import type {IsBooleanLiteral} from 'type-fest';
143+
144+
const id = 123;
145+
146+
type GetId<AsString extends boolean> =
147+
IsBooleanLiteral<AsString> extends true
148+
? AsString extends true
149+
? `${typeof id}`
150+
: typeof id
151+
: number | string;
152+
153+
function getId<AsString extends boolean = false>(options?: {asString: AsString}) {
154+
return (options?.asString ? `${id}` : id) as GetId<AsString>;
155+
}
156+
157+
const numberId = getId();
158+
//=> 123
159+
160+
const stringId = getId({asString: true});
161+
//=> '123'
162+
163+
declare const runtimeBoolean: boolean;
164+
const eitherId = getId({asString: runtimeBoolean});
165+
//=> number | string
166+
```
167+
168+
@category Utilities
169+
@category Type Guard
170+
*/
171+
export type IsBooleanLiteral<T> = LiteralCheck<T, boolean>;
172+
173+
/**
174+
Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
175+
176+
Useful for:
177+
- providing strongly-typed functions when given literal arguments
178+
- type utilities, such as when constructing parsers and ASTs
179+
180+
@example
181+
```
182+
import type {IsSymbolLiteral} from 'type-fest';
183+
184+
type Get<Obj extends Record<symbol, number>, Key extends keyof Obj> =
185+
IsSymbolLiteral<Key> extends true
186+
? Obj[Key]
187+
: number;
188+
189+
function get<Obj extends Record<symbol, number>, Key extends keyof Obj>(o: Obj, key: Key) {
190+
return o[key] as Get<Obj, Key>;
191+
}
192+
193+
const symbolLiteral = Symbol('literal');
194+
const symbolValue: symbol = Symbol('value');
195+
196+
get({[symbolLiteral]: 1} as const, symbolLiteral);
197+
//=> 1
198+
199+
get({[symbolValue]: 1} as const, symbolValue);
200+
//=> number
201+
```
202+
203+
@category Utilities
204+
@category Type Guard
205+
*/
206+
export type IsSymbolLiteral<T> = LiteralCheck<T, symbol>;
207+
208+
/** Helper type for `IsLiteral`. */
209+
type IsLiteralUnion<T> =
210+
| IsStringLiteral<T>
211+
| IsNumericLiteral<T>
212+
| IsBooleanLiteral<T>
213+
| IsSymbolLiteral<T>;
214+
215+
/**
216+
Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types).
217+
218+
Useful for:
219+
- providing strongly-typed functions when given literal arguments
220+
- type utilities, such as when constructing parsers and ASTs
221+
222+
@example
223+
```
224+
import type {IsLiteral} from 'type-fest';
225+
226+
// https://github.com/inocan-group/inferred-types/blob/master/src/types/string-literals/StripLeading.ts
227+
export type StripLeading<A, B> =
228+
A extends string
229+
? B extends string
230+
? IsLiteral<A> extends true
231+
? string extends B ? never : A extends `${B & string}${infer After}` ? After : A
232+
: string
233+
: A
234+
: A;
235+
236+
function stripLeading<Input extends string, Strip extends string>(input: Input, strip: Strip) {
237+
return input.replace(`^${strip}`, '') as StripLeading<Input, Strip>;
238+
}
239+
240+
stripLeading('abc123', 'abc');
241+
//=> '123'
242+
243+
const str = 'abc123' as string;
244+
245+
stripLeading(str, 'abc');
246+
//=> string
247+
```
248+
249+
@category Utilities
250+
@category Type Guard
251+
*/
252+
export type IsLiteral<T extends Primitive> = IsNotFalse<IsLiteralUnion<T>>;

‎test-d/internal.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {expectType} from 'tsd';
2-
import type {IsWhitespace, IsNumeric} from '../source/internal';
2+
import type {IsWhitespace, IsNumeric, IsNotFalse} from '../source/internal';
33

44
expectType<IsWhitespace<''>>(false);
55
expectType<IsWhitespace<' '>>(true);
@@ -27,3 +27,11 @@ expectType<IsNumeric<' 1.2'>>(false);
2727
expectType<IsNumeric<'1 2'>>(false);
2828
expectType<IsNumeric<'1_200'>>(false);
2929
expectType<IsNumeric<' 1 '>>(false);
30+
31+
expectType<IsNotFalse<true>>(true);
32+
expectType<IsNotFalse<boolean>>(true);
33+
expectType<IsNotFalse<true | false>>(true);
34+
expectType<IsNotFalse<true | false | false | false>>(true);
35+
expectType<IsNotFalse<false>>(false);
36+
expectType<IsNotFalse<false | false>>(false);
37+
expectType<IsNotFalse<false | false | false | false>>(false);

‎test-d/is-literal.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {expectError, expectType} from 'tsd';
2+
import type {
3+
IsLiteral,
4+
IsStringLiteral,
5+
IsNumericLiteral,
6+
IsBooleanLiteral,
7+
IsSymbolLiteral,
8+
} from '../index';
9+
10+
const stringLiteral = '';
11+
const numberLiteral = 1;
12+
// @ts-expect-error: suppress BigInt literal tsd warning
13+
const bigintLiteral = 1n;
14+
const booleanLiteral = true;
15+
const symbolLiteral = Symbol('');
16+
17+
declare const _string: string;
18+
declare const _number: number;
19+
declare const _bigint: bigint;
20+
declare const _boolean: boolean;
21+
declare const _symbol: symbol;
22+
23+
// Literals should be true
24+
expectType<IsLiteral<typeof stringLiteral>>(true);
25+
expectType<IsLiteral<typeof numberLiteral>>(true);
26+
expectType<IsLiteral<typeof bigintLiteral>>(true);
27+
expectType<IsLiteral<typeof booleanLiteral>>(true);
28+
expectType<IsLiteral<typeof symbolLiteral>>(true);
29+
30+
// Primitives should be false
31+
expectType<IsLiteral<typeof _string>>(false);
32+
expectType<IsLiteral<typeof _number>>(false);
33+
expectType<IsLiteral<typeof _bigint>>(false);
34+
expectType<IsLiteral<typeof _boolean>>(false);
35+
expectType<IsLiteral<typeof _symbol>>(false);
36+
37+
// Null, undefined, and non-primitives should fail all literal checks
38+
expectType<IsLiteral<null>>(false);
39+
expectType<IsLiteral<undefined>>(false);
40+
expectType<IsLiteral<any>>(false);
41+
expectType<IsLiteral<never>>(false);
42+
43+
expectType<IsStringLiteral<typeof stringLiteral>>(true);
44+
expectType<IsStringLiteral<typeof _string>>(false);
45+
46+
expectType<IsNumericLiteral<typeof numberLiteral>>(true);
47+
expectType<IsNumericLiteral<typeof bigintLiteral>>(true);
48+
expectType<IsNumericLiteral<typeof _number>>(false);
49+
expectType<IsNumericLiteral<typeof _bigint>>(false);
50+
51+
expectType<IsBooleanLiteral<typeof booleanLiteral>>(true);
52+
expectType<IsBooleanLiteral<typeof _boolean>>(false);
53+
54+
expectType<IsSymbolLiteral<typeof symbolLiteral>>(true);
55+
expectType<IsSymbolLiteral<typeof _symbol>>(false);
56+
57+
declare const anything: any;
58+
59+
// Missing generic parameter
60+
expectError<IsLiteral>(anything);
61+
expectError<IsStringLiteral>(anything);
62+
expectError<IsNumericLiteral>(anything);
63+
expectError<IsBooleanLiteral>(anything);
64+
expectError<IsSymbolLiteral>(anything);

0 commit comments

Comments
 (0)
Please sign in to comment.