I have a code like this:
// core
export type HasDuplicate<T extends Array<unknown>> = T extends [
infer L,
...infer R,
]
? L extends R[number]
? true
: HasDuplicate<R>
: false;
export type UniqArray<T> = HasDuplicate<T> extends false ? T : never;
// game
type CardRank =
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
| 'T'
| 'J'
| 'Q'
| 'K'
| 'A';
type CardSuit = '♥' | '♦' | '♠' | '♣';
class Card {
constructor(
private readonly _rank: CardRank,
private readonly _suit: CardSuit,
) {}
get id() {
return `${this._rank}:${this._suit}` as const;
}
}
class Hand {
constructor(
private readonly _cards: UniqArray<[Card, Card, Card, Card, Card]>,
) {}
}
new Hand([
new Card('2', '♠'),
new Card('3', '♠'),
new Card('4', '♠'),
new Card('5', '♠'),
new Card('6', '♠'),
]);
Why UniqArray
infer never
but should infer T
?
Evaluating UniqArray<T>
where T
is a finite-length tuple type produces T
only if each element of the tuple is a distinct type (roughly speaking, of course. In fact, UniqArray<[number, 1]>
produces [number, 1]
while UniqArray<[1, number]>
produces never
, which is inconsistent and probably not desired). But in your example, the type [Card, Card, Card, Card, Card]
is a tuple of five identical types. Those aren't distinct. So you get never
.
That's the answer to the question as asked.
It looks like what you are trying to do is have a Card
actually keep track of which rank and suit it represents, and then a Hand
has to keep track of which Card
s it's holding. For that to work you probably want to make both Card
and Hand
generic:
class Card<R extends CardRank = CardRank, S extends CardSuit = CardSuit> {
constructor(
private readonly _rank: R,
private readonly _suit: S,
) { }
}
class Hand<C extends [Card, Card, Card, Card, Card]> {
constructor(
private readonly _cards: UniqArray<C>,
) { }
}
Now when you write new Card('2', '♠')
you get a Card<"2", "♠">
, which is a distinct type from Card<"3", "♠">
. And when you write new Hand([new Card('2', '♠'), new Card('3', '♠'), ⋯])
you get a Hand
where the first two elements of the generic C
type argument are distinct. This now lets you validate that Hand
is made of five distinct cards (again, roughly speaking):
new Hand([
new Card('2', '♠'),
new Card('3', '♠'),
new Card('4', '♠'),
new Card('5', '♠'),
new Card('6', '♠'),
]); // okay
new Hand([
new Card('2', '♠'),
new Card('3', '♠'),
new Card('4', '♠'),
new Card('2', '♠'),
new Card('6', '♠'),
]); // error!
The top new Hand
works because the five cards are distinct, while the bottom one fails because it has two Card<"2", "♠">
elements.
Note that this isn't necessarily the "right" way to write Card
and Hand
; maybe instead of UniqArray
you want a utility type that actually complains about your first repeat instead of the whole input. Or maybe there are other edge cases that UniqArray
doesn't work for. But I consider such issues out of scope for the question as asked.