2

I am just digging into Typescript typings and I wondered how to define a type which is a tuple but with unordered element types.

I mean, having

type SimpleTuple = [number, string];

const tup1: SimpleTuple = [7, `7`]; // Valid
const tup2: SimpleTuple = [`7`, 7]; // 'string' is not assignable to 'number'
                                    // and vice-versa

This is useful in many cases, but what if I don't care about order or I need it to be unordered.
The example above is quite trivial since I could define

type SimpleUnorderedTuple = [number, string] | [string, number];

const tup1: SimpleUnorderedTuple = [7, `7`]; // Valid
const tup2: SimpleUnorderedTuple = [`7`, 7]; // Valid

However, I may have a bunch of types... A combinatory logic uppon them would be painful

type ABunchOfTypes = 'these' | 'are' | 'some' | 'words' | 'just' | 'for' | 'the' | 'example';
type ComplexUnorderedTuple =
    ['these', 'are', 'some', 'words', 'just', 'for', 'the', 'example'] |
    ['these', 'are', 'some', 'words', 'just', 'for', 'example', 'the'] |
    // and so on ...

This is insane. There are !n possible combinations, where n is the number of elements (I guess, I am not too good at maths!).

I am trying to achieve something like

type ABunchOfTypes = 'these' | 'are' | 'some';
type UnorderedTuple<T> = ; //...

type ComplexUnorderedTuple = UnorderedTuple<ABunchOfTypes>;

I found in this article

Any subsequent value we add to the tuple variable can be any of the predefined tuple types in no particular order.

But I couldn't reproduce. If I define a tuple of two elements, I am not allowed to access to the nth position, if n is greater than (or equal) the tuple length.

VRoxa
  • 973
  • 6
  • 25
  • A tuple is nothing more than an array with a specified length and type of every element. Since you explicitly don't want the specified type part from a tuple, then I believe what you are looking for is actually a way to [declare a fixed length array](https://stackoverflow.com/questions/41139763/how-to-declare-a-fixed-length-array-in-typescript). – jsamol May 17 '20 at 18:34
  • I want the specified type. If I declare a type to be `['word1', 'word2']` I am constraining the tuple to contain only these two words. It's just I don't care the order of them. Declaring a fixed-length-array won't solve this because it could admit the repetition of elements `['word1', 'word2']` which I don't want. – VRoxa May 17 '20 at 18:44
  • See [this comment](https://stackoverflow.com/a/60762482/9614175), it seems to be what you are looking for. – Stéphane Veyret Jul 24 '20 at 09:42
  • You wrote "If I define a tuple of two elements, I am not allowed to access to the nth position, if n is greater than (or equal) the tuple length." so it seems you don't need a two elements tuple, but something else. Please provide some more examples of valid data you need – Daniele Ricci Jul 24 '20 at 09:58
  • This feels a lot like an XY problem, but... does this help? https://github.com/microsoft/TypeScript/issues/13298#issuecomment-654906323 – danielnixon Jul 27 '20 at 05:17

3 Answers3

3

If you are looking for permutation type of a union, this will give you exactly that:

type ABunchOfTypes = 'these' | 'are' | 'some' | 'words' | 'just';

type PushFront<TailT extends any[], HeadT> =
    ((head: HeadT, ...tail: TailT) => void) extends ((...arr: infer ArrT) => void) ? ArrT : never;

type CalculatePermutations<U extends string, ResultT extends any[] = []> = {
    [k in U]: (
        [Exclude<U, k>] extends [never] ?
        PushFront<ResultT, k> :
        CalculatePermutations<Exclude<U, k>, PushFront<ResultT, k>>
    )
}[U];

var test: CalculatePermutations<ABunchOfTypes> = ['are', 'these', 'just', 'words', 'some'];

You can try it in this Playground.

There is a limitation to this approach, however; my experiment showed that TypeScript can at most process a union of 7 strings. With 8 or more strings, an error will shown.

Update

If "no repetition" is what is needed, it is a lot simpler.

type ABunchOfTypes = 'these' | 'are' | 'some' | 'words' | 'just';

type PushFront<TailT extends any[], HeadT> =
    ((head: HeadT, ...tail: TailT) => void) extends ((...arr: infer ArrT) => void) ? ArrT : never;

type NoRepetition<U extends string, ResultT extends any[] = []> = {
    [k in U]: PushFront<ResultT, k> | NoRepetition<Exclude<U, k>, PushFront<ResultT, k>>
}[U];

// OK
var test: NoRepetition<ABunchOfTypes> = ['are', 'these', 'just', 'words', 'some'];
test = ['are', 'these', 'just'];
test = ['are'];

// Not OK
test = ['are', 'these', 'are'];

See this Playground Link.

Also, with the upcoming TypeScript 4 syntax, it can be simplified still:

type ABunchOfTypes = 'these' | 'are' | 'some' | 'words' | 'just';

// for TypeScript 4 
type NoRepetition<U extends string, ResultT extends any[] = []> = {
    [k in U]: [k, ...ResultT] | NoRepetition<Exclude<U, k>, [k, ...ResultT]>
}[U];

// OK
var test: NoRepetition<ABunchOfTypes> = ['are', 'these', 'just', 'words', 'some'];
test = ['are', 'these', 'just'];
test = ['are'];

// Not OK
test = ['are', 'these', 'are'];

See this Playground Link.

Update 2

The above two assumes that you require the array to be non-empty. If you also want to allow empty array, you can do it like this:

type ABunchOfTypes = 'these' | 'are' | 'some' | 'words' | 'just';

// for TypeScript 4 
type NoRepetition<U extends string, ResultT extends any[] = []> = ResultT | {
    [k in U]: NoRepetition<Exclude<U, k>, [k, ...ResultT]>
}[U];

// OK
var test: NoRepetition<ABunchOfTypes> = ['are', 'these', 'just', 'words', 'some'];
test = ['are', 'these', 'just'];
test = ['are'];
test = [];

// Not OK
test = ['are', 'these', 'are'];

See this Playground Link.

It is possible to replace U extends string by U extends keyof any to support union types of strings, numbers and symbols, but the current limitation of TypeScript makes it impossible to go beyond this.

Mu-Tsun Tsai
  • 2,348
  • 1
  • 12
  • 25
  • Hi, @Mu-Tsun. That's a very good point and quite accurate. The 7-strings constraint is not a problem, indeed. The only thing I really miss is how to make optional tuple-elements. I mean, having the same example above, how to make `const t: Type` = ['are']` valid. Thanks! – VRoxa Jul 28 '20 at 13:28
  • That's an awesome answer. I would never figured it out! And yeah, I will use it like `U extends string | number | symbol`. Thanks a lot. – VRoxa Jul 29 '20 at 11:43
  • Awesome solution, but If `ABunchOfTypes` contains 9 or more than 9 words, a `Type instantiation is excessively deep and possibly infinite.` error occurred. – fenyiwudian Jan 11 '21 at 09:28
0

Mu-Tsun Tsai's answer seems to be a good starting point.

type ABunchOfTypes = 'these' | 'are' | 'some' | 'words' | 'just';

type PushFront<TailT extends any[], HeadT> =
((head: HeadT, ...tail: TailT) => void) extends ((...arr: infer ArrT) => void) ? ArrT : never;

type CalculatePermutations<U extends string, ResultT extends any[] = []> = {
    [k in U]: (
        [Exclude<U, k>] extends [never] ?
        PushFront<ResultT, k> :
        CalculatePermutations<Exclude<U, k>, PushFront<ResultT, k>>
    ) | PushFront<ResultT, k>
}[U];

var test1: CalculatePermutations<ABunchOfTypes> = ['are', 'these', 'just', 'words', 'some'];
var test2: CalculatePermutations<ABunchOfTypes> = ['are', 'just', 'words'];
// next gives error
var test3: CalculatePermutations<ABunchOfTypes> = ['are', 'are'];
Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55
  • Hi, @Daniele. The point of the question is the constraint of type appearance, not the length of the tuple. In your example, `const d: T = ['are', 'are']` is valid, which I don't want it to be because of the repetition. – VRoxa Jul 28 '20 at 13:31
  • So @VRoxa what you need is a variable length array where each type of `ABunchOfTypes` can appear only once, is it right? – Daniele Ricci Jul 28 '20 at 14:54
  • That's exactly it, yeah. – VRoxa Jul 28 '20 at 14:55
  • @DanieleRicci I appreciate that you found my answer inspiring, but it can actually go simpler than that; check out the update of my answer. – Mu-Tsun Tsai Jul 28 '20 at 21:58
  • Gr8 job @Mu-TsunTsai! I already upvoted your answer, I would have upvoted once again! But I want to launch you another challenge: are able to give us a solution with following: `type ABunchOfTypes = 'some' | 'types' | number;` ? – Daniele Ricci Jul 28 '20 at 22:10
  • @DanieleRicci Sure! The trick is to replace `U extends string` by `U extends keyof any`, and then you can use string, number, or symbol in your union. Unfortunately you can't go beyond that, due to the limitation of indices in TypeScript. – Mu-Tsun Tsai Jul 28 '20 at 22:15
  • Simply awesome @Mu-TsunTsai ! – Daniele Ricci Jul 28 '20 at 22:25
0

Create an enumerate function like this:

type ValueOf<T> = T[keyof T];

type NonEmptyArray<T> = [T, ...T[]]

type MustInclude<T, U extends T[]> =
  [T] extends [ValueOf<U>]
    ? U
    : never;

const enumerate = <T>() =>
  <U extends NonEmptyArray<T>>(...elements: MustInclude<T, U>) =>
    elements;

Usage:

type Word = 'these' | 'are' | 'some' | 'words';

const test1 = enumerate<Word>()('are', 'some', 'these', 'words');
const test2 = enumerate<Word>()('words', 'these', 'are', 'some');
const test3 = enumerate<Word>()('these', 'are', 'some', 'words');
  • ✅ Empty lists are not allowed
  • ✅ All values must be present
  • ✅ Duplicates are not allowed
  • ✅ Every value must be a Word

Playground link

Karol Majewski
  • 23,596
  • 8
  • 44
  • 53
  • Actually your code does not eliminates duplicates; it just says that every value must be presented. Try `enumerate()('are', 'some', 'these', 'words', 'some')` and it's valid. – Mu-Tsun Tsai Jul 29 '20 at 00:03