type-fest icon indicating copy to clipboard operation
type-fest copied to clipboard

`LiteralList<Union>`

Open macmillen opened this issue 1 year ago • 5 comments

I want to be able to define an array that contains exactly the elements of all possible values of a union once.

type Union = "prevStep" | "nextStep" | "submit" | "order";

// this works:
const list: LiteralList<Union> = ["nextStep", "order", "prevStep", "submit"];

// this fails:
const list: LiteralList<Union> = ["nextStep", "order", "prevStep", "submit", "submit"];

// this fails:
const list: LiteralList<Union> = ["nextStep", "order", "prevStep"];

I already made this work with this type:

type LiteralList<T extends string, U = T> = [T] extends [never]
  ? []
  : U extends T
  ? [U, ...LiteralList<Exclude<T, U>>]
  : [];

I'm not sure though if it makes sense since I asked ChatGPT to do it.

Is it possible to add this type helper to type-fest?

macmillen avatar Jul 03 '24 22:07 macmillen

I realized that this:

type LiteralList<T extends string, U = T> = [T] extends [never]
  ? []
  : U extends T
  ? [U, ...LiteralList<Exclude<T, U>>]
  : [];

for some reason is really slowing down the TS language server.

Is there maybe another way to achieve this?

macmillen avatar Jul 04 '24 08:07 macmillen

Related to https://github.com/sindresorhus/type-fest/pull/686

Emiyaaaaa avatar Jul 05 '24 02:07 Emiyaaaaa

@macmillen The LiteralList type generates all possible combinations of tuples for a given union type, which will grow quite quickly. For example, for a union of 3 members, LiteralList produces a union of 6 (3!) tuples and for a union of 6 members, it results in 720 (6!) tuples.

So, LiteralList is practical only for small unions, upto 4/5 members.

type T = LiteralList<"a" | "b" | "c">;
//   ^? type T = ["a", "b", "c"] | ["a", "c", "b"] | ["b", "a", "c"] | ["b", "c", "a"] | ["c", "a", "b"] | ["c", "b", "a"]

@sindresorhus WDYT, should we add this type, probably with a check that ensures the union is small enough?

som-sm avatar Feb 12 '25 07:02 som-sm

Sure, we can add it for small sets of values. I'm not sold on the name though.

Some alternatives:

  • UnionPermutation
  • ExhaustiveTuple
  • ExhaustiveList
  • CompleteList
  • AllCases

sindresorhus avatar Feb 20 '25 13:02 sindresorhus

@macmillen as @som-sm said this type is not practical to add the way u suggested. But u can instead make a validator that does the same by checking if the List giving is having one of each member in the Union as follows:

Type

// Helpers
type JoinUnion<U, S extends string = ','> =
    UnionToTuple<U> extends infer Tuple extends JoinableItem[]
        ? Join<Tuple, S>
        : '';

type TupleOfUnions<U> = UnionToTuple<U>['length'] extends infer Length extends number
    ? Readonly<BuildTuple<Length, U>>
    : never;

type TupleAsString<T, S extends string = '\', \''> = `['${
    [T] extends [JoinableItem[]]
        ? Join<T, S>
        : JoinUnion<T, S>
}']`;

// Main Type
type LiteralList<
    T extends readonly any[],
    U extends readonly any[] | any,
> = (
    ([U] extends [readonly any[]] ? U : TupleOfUnions<U>) extends infer V extends readonly any[]
        ? V['length'] extends T['length']
            ? Exclude<T[number], V[number]> extends infer TnV
                ? Exclude<V[number], T[number]> extends infer VnT
                    ? IsNever<TnV> extends true
                        ? IsNever<VnT> extends true
                            ? T
                            : never | `Type ${TupleAsString<T>} is missing Properties: ${TupleAsString<VnT>}`
                        : never | `Type ${TupleAsString<T>} has extra Properties: ${TupleAsString<TnV>}`
                    : never
                : never
            : never | `Type ${TupleAsString<T>} is not the required Length of: ${V['length']}`
        : never
);

Use:


type Union = 'a' | 'b' | 'c' | 'd';
type UnionList = TupleOfUnions<Union>;
//   ^? [Union, Union, Union, Union]

type foo1 = LiteralList<['a', 'b', 'c', 'd'], Union>; // u can pass in a Union
//    ^? ['a', 'b', 'c', 'd']

type foo2 = LiteralList<['a', 'b', 'c'], UnionList>; // or pass in a Tuple made of that Union
//    ^? never | Type ['a', 'b', 'c'] is not the required Length of: 4

type foo3 = LiteralList<['a', 'b', 'c', 'd', 'e'], Union>;
//    ^? never | Type ['a', 'b', 'c', 'd', 'e'] is not the required Length of: 4

type foo4 = LiteralList<['a', 'b', 'c', 'e'], UnionList>;
//    ^? never | Type ['a', 'b', 'c', 'e'] has extra Properties: ['e']

type foo5 = LiteralList<['a', 'b', 'd', 'd'], UnionList>;
//    ^? never | Type ['a', 'b', 'd', 'd'] is missing Properties: ['c']

declare function func<T extends Union[] /* or UnionList */>(x: T): typeof x;
const bar1 = ['b', 'd', 'a', 'c'] as const
const bar2 = ['a', 'b', 'c', 'c'] as const

func(bar1 satisfies LiteralList<typeof bar1, Union>); // Fine
func(bar2 satisfies LiteralList<typeof bar2, Union>); // Error: Type ['a', 'b', 'c', 'c'] is missing Properties: ['d']

The string that get returned are just visual Error like They can be removed.

benzaria avatar Jun 02 '25 18:06 benzaria