问题
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.
回答1:
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.
回答2:
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'];
回答3:
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
来源:https://stackoverflow.com/questions/61852773/unordered-tuple-type