I am trying to create a type alias in typescript to which all values are assignable that are arrays of Pairs and where each pair matches the succeeding pair like domino bricks do: Pair<A,B> matches Pair<C,D> if and only if B = C.
Example values:
const chain1: DominoChain = [ ["A","B"], ["B","C"], ["C","D"], ["D","E"] ] // OK
const chain2: DominoChain = [ [3, 4], [4, 2], [2, 1] ] // OK
const chain3: DominoChain = [ [3, null], [null, {}], [{}, undefined] ] // OK
const chain1: DominoChain = [ [1, 2] ] // OK
const chain1: DominoChain = [ ] // OK
const chain1: DominoChain = [ [1, 2], [3, 4] ] // Not OK because 2 !== 3. The compiler should raise an error
Here is what I have so far, but it is not working:
type Domino<A,B> = [A, B]
type DominoChain<A = any, B = any> = [Domino<A, B>] | [Domino<A, B>, ...DominoChain<B, any>]
I don't known how to properly define the matching constraint between two Domino pairs and I'm doing something wrong with the recursion part.
CodePudding user response:
It is not possible to express in typescript type system such restriction without validating generic argument.
However, it is possible to create a type utility which will validate your input.
Consider this example:
type Last<T> = T extends [...infer _, infer LastElem] ? LastElem : never
type Head<T> = T extends [infer First, ...infer _] ? First : never
type IsChainable<Current, Next> =
(Last<Current> extends Head<Next>
? (Head<Next> extends Last<Current>
? true
: false)
: false)
type DominoChain<List extends any[], Result extends any[] = []> =
(List extends []
? Result
: (List extends [infer First]
? (IsChainable<Last<Result>, First> extends true
? [...Result, First]
: never)
: (List extends [infer First, ...infer Rest]
? (Result extends []
? DominoChain<Rest, [...Result, First]>
: (IsChainable<First, Head<Rest>> extends true
? DominoChain<Rest, [...Result, First]>
: never))
: never
)
)
)
type Test1 = DominoChain<[["A", "B"], ["B", "C"], ["C", "D"], ["D", "E"]]>
type Test2 = DominoChain<[[3, 4], [4, 2], [2, 1]]>
type Test3 = DominoChain<[[3, null], [null, {}], [{}, undefined]]>
type Test4 = DominoChain<[[1, 2]]> // ok
type Test5 = DominoChain<[]> // ok
type Test6 = DominoChain<[[1, 2], [3, 4],]> // never
const domino = <
Elem extends string,
Tuple extends Elem[],
List extends [...Tuple][]
>(list: [...List] & DominoChain<[...List]>) => list
domino([["A", "B"], ["B", "C"], ["C", "D"], ["D", "E"]]) // ok
domino([[1, 2], [3, 4],]) // error
Last
- obtains last element in the tuple
Head
- obtains first element in the tuple
IsChainable
- checks whether last element in current tuple extends first element from the next tuple and vice versa. Pretty straightforward.
DominoChain
- iterates through list of tuples and calls IsChainable
on each tuple.
It is a lot of code but it is really not so complicated once you get it.
CodePudding user response:
With the hint from @captain-yossarian that you need to add an additional function or constant for this type checking to work, I created this solution:
type Domino<A = any, B = any> = readonly [A,B]
type DominoChain<T extends readonly Domino[], FIRST = any> =
T extends readonly [Domino<any, infer A>, ... infer REST]
? (REST extends Domino[] ? readonly [Domino<FIRST, A>, ...DominoChain<REST, A>] : never)
: T
function dominoTest<T extends readonly Domino[]>(domino: DominoChain<T>): T {
return domino
}
dominoTest(<const>[ ["A","B"], ["B","C"], ["C","D"], ["D","E"] ]) // OK
dominoTest(<const>[ [3, 4], [4, 2], [2, 1] ]) // OK
dominoTest(<const>[ [3, null], [null, {}], [{}, undefined] ]) // OK
dominoTest(<const>[ [1, 2] ]) // OK
dominoTest(<const>[ ]) // OK
dominoTest(<const>[ [1, 2], [3, 4] ]) // error
// TS2345: Argument of type
// 'readonly [readonly [1, 2], readonly [3, 4]]'
// is not assignable to parameter of type
// 'readonly [Domino<any, 2>, Domino<2, 4>]'.
// Type at position 1 in source is not compatible with type at position 1 in target.
// Type '3' is not assignable to type '2'.