Skip to content

Commit 7396c78

Browse files
committed
Add LiteralOf type
1 parent aab1e00 commit 7396c78

File tree

4 files changed

+166
-2
lines changed

4 files changed

+166
-2
lines changed

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './sour
3939
export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep.d.ts';
4040
export type {ReadonlyDeep} from './source/readonly-deep.d.ts';
4141
export type {LiteralUnion} from './source/literal-union.d.ts';
42-
export type {LoosenUnion} from './source/loosen-union.d.ts';
42+
export type {LiteralOf} from './source/literal-of.d.ts';
4343
export type {Promisable} from './source/promisable.d.ts';
4444
export type {Arrayable} from './source/arrayable.d.ts';
4545
export type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged} from './source/tagged.d.ts';

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Click the type names for complete docs.
122122
- [`UndefinedOnPartialDeep`](source/undefined-on-partial-deep.d.ts) - Create a deep version of another type where all optional keys are set to also accept `undefined`.
123123
- [`ReadonlyDeep`](source/readonly-deep.d.ts) - Create a deeply immutable version of an `object`/`Map`/`Set`/`Array` type. Use [`Readonly<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype) if you only need one level deep.
124124
- [`LiteralUnion`](source/literal-union.d.ts) - Create a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).
125-
- [`LoosenUnion`](source/loosen-union.d.ts) - Create a union by removing all Non-Literal primitive types from a union, while retaining literal types.
125+
- [`LiteralOf`](source/literal-of.d.ts) - Creates a union of the literal members from a given union type, removing wide primitive or infinite types, and optionally specify the Strictness.
126126
- [`Tagged`](source/tagged.d.ts) - Create a [tagged type](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d) that can support [multiple tags](https://github.com/sindresorhus/type-fest/issues/665) and [per-tag metadata](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf). (This replaces the previous [`Opaque`](source/tagged.d.ts) type, which is now deprecated.)
127127
- [`UnwrapTagged`](source/tagged.d.ts) - Get the untagged portion of a tagged type created with `Tagged`. (This replaces the previous [`UnwrapOpaque`](source/tagged.d.ts) type, which is now deprecated.)
128128
- [`InvariantOf`](source/invariant-of.d.ts) - Create an [invariant type](https://basarat.gitbook.io/typescript/type-system/type-compatibility#footnote-invariance), which is a type that does not accept supertypes and subtypes.

source/literal-of.d.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { UnwrapTagged, TagContainer } from './tagged.js';
2+
import type { IsLiteral } from './is-literal.js';
3+
import type { Primitive } from './primitive.js';
4+
import type { Not } from './internal/type.js';
5+
import type { IsEqual } from './is-equal.js';
6+
import type { ValueOf } from './value-of.js';
7+
8+
/**
9+
Creates a union of the literal members from a given union type, removing wide primitive or infinite types.
10+
11+
This utility helps you extract only the literal members from a "literal union" type
12+
(e.g., `'foo' | 'bar' | string` becomes `'foo' | 'bar'`), saving you from defining separate types for literals and unions.
13+
14+
It works with all primitive and tagged types, and supports two strictness modes:
15+
16+
- **Strict mode (`Strict = true`)**: Removes any infinite signature type (e.g., `abc${string}`, `123${number}`).
17+
- **Non-strict mode (`Strict = false`)**: Removes only wide primitive types (e.g., `string`, `number`).
18+
19+
@default true
20+
21+
@example
22+
```ts
23+
import type { LiteralOf } from 'type-fest';
24+
25+
// String example:
26+
type Pet = LiteralUnion<'dog' | 'cat' | `${string}Dog`, string>;
27+
// ^? type Pet = 'dog' | 'cat' | `${string}Dog` | (string & {})
28+
type PetLiteralStrict = LiteralOf<Pet>;
29+
// ^? type PetLiteralNonStrict = 'dog' | 'cat'
30+
type PetLiteralNonStrict = LiteralOf<Pet, false>;
31+
// ^? type PetLiteralStrict = 'dog' | 'cat' | `${string}Dog`
32+
33+
// Number example:
34+
type Nums = LiteralUnion<0 | 1 | 2, number>;
35+
// ^? type Nums = 0 | 1 | 2 | (number & {})
36+
type NumsLiteral = LiteralOf<Nums>;
37+
// ^? type NumsLiteral = 0 | 1 | 2
38+
39+
// Symbol example:
40+
declare const sym1: unique symbol;
41+
declare const sym2: unique symbol;
42+
type Symbols = LiteralUnion<typeof sym1 | typeof sym2, symbol>;
43+
// ^? type Symbols = typeof sym1 | typeof sym2 | (symbol & {})
44+
type SymbolsLiteral = LiteralOf<Symbols>;
45+
// ^? type SymbolsLiteral = typeof sym1 | typeof sym2
46+
47+
// BigInt example:
48+
type Big = LiteralUnion<1n | 2n, bigint>;
49+
// ^? type Big = 1n | 2n | (bigint & {})
50+
type BigLiteral = LiteralOf<Big>;
51+
// ^? type BigLiteral = 1n | 2n
52+
53+
```
54+
55+
@author benzaria
56+
@see LiteralUnion
57+
@category Type
58+
*/
59+
export type LiteralOf<
60+
LiteralUnion extends Primitive,
61+
Strict extends boolean = true,
62+
> = ValueOf<{
63+
[P in Primitive as string]: LiteralUnion extends P
64+
? _LiteralOf<LiteralUnion, P, Strict>
65+
: never
66+
}>;
67+
68+
type _LiteralOf<U, P, S> = ValueOf<{
69+
[K in U as string]: (
70+
S extends true
71+
? Not<IsLiteral<
72+
K extends TagContainer<{ [J in PropertyKey]: any }>
73+
? UnwrapTagged<K>
74+
: K
75+
>>
76+
: IsEqual<K, (P & Record<never, never>)>
77+
) extends true
78+
? never
79+
: K
80+
}>;
81+
82+
/**
83+
84+
? Explanation:
85+
86+
- LiteralOf iterates over all primitive types (string, number, symbol, bigint, boolean).
87+
- For each, if the input type extends that primitive, applies _LiteralOf.
88+
- _LiteralOf iterates over the union members:
89+
- In non-strict mode, removes (type & {}) (the canonical "widened" type).
90+
- In strict mode, removes any non-literal (infinite template literal types).
91+
- Otherwise, keeps the literal member.
92+
93+
*/

test-d/literal-of.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expectType } from 'tsd';
2+
import type { LiteralOf, LiteralUnion, IsEqual, Tagged } from '../index.js';
3+
4+
// For Strings:
5+
6+
type Events = LiteralUnion<'hover' | 'click' | `click-${number}`, string>;
7+
// ^?
8+
type EventsLiteralStrict = LiteralOf<Events>;
9+
// ^?
10+
type EventsLiteralNonStrict = LiteralOf<Events, false>;
11+
// ^?
12+
13+
expectType<IsEqual<'hover' | 'click' | `click-${number}`, EventsLiteralNonStrict>>(true);
14+
expectType<IsEqual<'hover' | 'click', EventsLiteralStrict>>(true);
15+
16+
expectType<IsEqual<'hover' | 'click' | `click-${number}`, EventsLiteralStrict>>(false);
17+
expectType<IsEqual<'hover' | 'click', EventsLiteralNonStrict>>(false);
18+
19+
// For Numbers:
20+
21+
type Nums = LiteralUnion<0 | 1 | 2, number>;
22+
// ^?
23+
type NumsLiteral = LiteralOf<Nums>;
24+
// ^?
25+
26+
expectType<IsEqual<0 | 1 | 2, NumsLiteral>>(true);
27+
expectType<IsEqual<0 | 1 | 2, Nums>>(false);
28+
29+
// For Symbols:
30+
31+
declare const symbol1: unique symbol;
32+
declare const symbol2: unique symbol;
33+
34+
type Symbol1 = typeof symbol1;
35+
type Symbol2 = typeof symbol2;
36+
37+
type Symbols = LiteralUnion<Symbol1 | Symbol2, symbol>;
38+
// ^?
39+
type SymbolsLiteral = LiteralOf<Symbols>;
40+
// ^?
41+
42+
expectType<IsEqual<Symbol1 | Symbol2, SymbolsLiteral>>(true);
43+
expectType<IsEqual<Symbol1 | Symbol2, Symbols>>(false);
44+
45+
// For Bigints:
46+
47+
type Big = LiteralUnion<1n | 2n, bigint>;
48+
// ^?
49+
type BigLiteral = LiteralOf<Big>;
50+
// ^?
51+
52+
expectType<IsEqual<1n | 2n, BigLiteral>>(true);
53+
expectType<IsEqual<1n | 2n, Big>>(false);
54+
55+
// For Tagged types:
56+
57+
type Animals = Tagged<'dog' | 'cat' | `${string}Dog`, 'alimals'>;
58+
type AnimalsStrict = Exclude<Animals, `${string}Dog`>;
59+
60+
type TaggedUnion = LiteralUnion<Animals, string>;
61+
// ^?
62+
type TaggedStrict = LiteralOf<TaggedUnion>;
63+
// ^?
64+
type TaggedNonStrict = LiteralOf<TaggedUnion, false>;
65+
// ^?
66+
67+
expectType<IsEqual<Animals, TaggedNonStrict>>(true);
68+
expectType<IsEqual<AnimalsStrict, TaggedStrict>>(true);
69+
70+
expectType<IsEqual<Animals, TaggedUnion>>(false);
71+
expectType<IsEqual<AnimalsStrict, TaggedUnion>>(false);

0 commit comments

Comments
 (0)