typescript

UniqArray infer wrong type


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 ?


Solution

  • 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 Cards 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.

    Playground link to code