Skip to content
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './sour
export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep.d.ts';
export type {ReadonlyDeep} from './source/readonly-deep.d.ts';
export type {LiteralUnion} from './source/literal-union.d.ts';
export type {LoosenUnion} from './source/loosen-union.d.ts';
export type {Promisable} from './source/promisable.d.ts';
export type {Arrayable} from './source/arrayable.d.ts';
export type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged} from './source/tagged.d.ts';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Click the type names for complete docs.
- [`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`.
- [`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.
- [`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).
- [`LoosenUnion`](source/loosen-union.d.ts) - Create a union by removing all Non-Literal primitive types from a union, while retaining literal types.
- [`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.)
- [`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.)
- [`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.
Expand Down
93 changes: 93 additions & 0 deletions source/loosen-union.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type {UnwrapTagged, TagContainer} from './tagged.js';
import type {Primitive} from './primitive.d.ts';
import type {IsLiteral} from './is-literal.js';
import type {Not} from './internal/type.d.ts';
import type {IsEqual} from './is-equal.d.ts';
import type {ValueOf} from './value-of.d.ts';

/**
Create a union by removing all Non-Literal primitive types from a union, while retaining literal types.

This utility helps you extract only the literal members from a "literal union" type
(e.g., `'foo' | 'bar' | string` becomes `'foo' | 'bar'`), saving you from defining separate types for literals and unions.

It works with all primitive and tagged types, and supports two strictness modes:

- **Strict mode (`Strict = true`)**: Removes any infinite signature type (e.g., `abc${string}`, `123${number}`), keeping only genuine literals.
- **Non-strict mode (`Strict = false`)**: Removes only wide primitive types (e.g., `string`, `number`).

@default false

@example
```ts
import type { LoosenUnion } from 'type-fest';

// String example:
type Pet = LiteralUnion<'dog' | 'cat' | `${string}Dog`, string>;
// ^? type Pet = 'dog' | 'cat' | `${string}Dog` | (string & {})
type PetLiteralNonStrict = LoosenUnion<Pet>;
// ^? type PetLiteralNonStrict = 'dog' | 'cat' | `${string}Dog`
type PetLiteralStrict = LoosenUnion<Pet, true>;
// ^? type PetLiteralStrict = 'dog' | 'cat'

// Number example:
type Nums = LiteralUnion<0 | 1 | 2, number>;
// ^? type Nums = 0 | 1 | 2 | (number & {})
type NumsLiteral = LoosenUnion<Nums>;
// ^? type NumsLiteral = 0 | 1 | 2

// Symbol example:
declare const sym1: unique symbol;
declare const sym2: unique symbol;
type Symbols = LiteralUnion<typeof sym1 | typeof sym2, symbol>;
// ^? type Symbols = typeof sym1 | typeof sym2 | (symbol & {})
type SymbolsLiteral = LoosenUnion<Symbols>;
// ^? type SymbolsLiteral = typeof sym1 | typeof sym2

// BigInt example:
type Big = LiteralUnion<1n | 2n, bigint>;
// ^? type Big = 1n | 2n | (bigint & {})
type BigLiteral = LoosenUnion<Big>;
// ^? type BigLiteral = 1n | 2n

```

@author benzaria
@see LiteralUnion
@category Type
*/
export type LoosenUnion<
LiteralUnion extends Primitive,
Strict extends boolean = false,
> = ValueOf<{
[P in Primitive as string]: LiteralUnion extends P
? _LoosenUnion<LiteralUnion, P, Strict>
: never
}>;

type _LoosenUnion<U, P, S> = ValueOf<{
[K in U as string]: (
S extends true
? Not<IsLiteral<
K extends TagContainer<{[J in PropertyKey]: any}>
? UnwrapTagged<K>
: K
>>
: IsEqual<K, (P & Record<never, never>)>
) extends true
? never
: K
}>;

/**

? Explanation:

- LoosenUnion iterates over all primitive types (string, number, symbol, bigint, boolean).
- For each, if the input type extends that primitive, applies _LoosenUnion.
- _LoosenUnion iterates over the union members:
- In non-strict mode, removes (type & {}) (the canonical "widened" type).
- In strict mode, removes any non-literal (infinite template literal types).
- Otherwise, keeps the literal member.

*/
71 changes: 71 additions & 0 deletions test-d/loosen-union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {expectType} from 'tsd';
import type {LoosenUnion, LiteralUnion, IsEqual, Tagged} from '../index.d.ts';

// For Strings:

type Events = LiteralUnion<'hover' | 'click' | `click-${number}`, string>;
// ^?
type EventsLiteralNonStrict = LoosenUnion<Events>;
// ^?
type EventsLiteralStrict = LoosenUnion<Events, true>;
// ^?

expectType<IsEqual<'hover' | 'click' | `click-${number}`, EventsLiteralNonStrict>>(true);
expectType<IsEqual<'hover' | 'click', EventsLiteralStrict>>(true);

expectType<IsEqual<'hover' | 'click' | `click-${number}`, EventsLiteralStrict>>(false);
expectType<IsEqual<'hover' | 'click', EventsLiteralNonStrict>>(false);

// For Numbers:

type Nums = LiteralUnion<0 | 1 | 2, number>;
// ^?
type NumsLiteral = LoosenUnion<Nums>;
// ^?

expectType<IsEqual<0 | 1 | 2, NumsLiteral>>(true);
expectType<IsEqual<0 | 1 | 2, Nums>>(false);

// For Symbols:

declare const symbol1: unique symbol;
declare const symbol2: unique symbol;

type Symbol1 = typeof symbol1;
type Symbol2 = typeof symbol2;

type Symbols = LiteralUnion<Symbol1 | Symbol2, symbol>;
// ^?
type SymbolsLiteral = LoosenUnion<Symbols>;
// ^?

expectType<IsEqual<Symbol1 | Symbol2, SymbolsLiteral>>(true);
expectType<IsEqual<Symbol1 | Symbol2, Symbols>>(false);

// For Bigints:

type Big = LiteralUnion<1n | 2n, bigint>;
// ^?
type BigLiteral = LoosenUnion<Big>;
// ^?

expectType<IsEqual<1n | 2n, BigLiteral>>(true);
expectType<IsEqual<1n | 2n, Big>>(false);

// For Tagged types:

type Animals = Tagged<'dog' | 'cat' | `${string}Dog`, 'alimals'>;
type AnimalsStrict = Exclude<Animals, `${string}Dog`>;

type TaggedUnion = LiteralUnion<Animals, string>;
// ^?
type TaggedLiteral = LoosenUnion<TaggedUnion>;
// ^?
type TaggedStrict = LoosenUnion<TaggedUnion, true>;
// ^?

expectType<IsEqual<Animals, TaggedLiteral>>(true);
expectType<IsEqual<AnimalsStrict, TaggedStrict>>(true);

expectType<IsEqual<Animals, TaggedUnion>>(false);
expectType<IsEqual<AnimalsStrict, TaggedUnion>>(false);