#37233•Hard

All of a Kind

Decide at the type level whether a tuple holds exactly one element of every member of a string union, using a recursive walk and Exclude as a checklist.

A union is a set, a tuple is a list, and this challenge asks whether the two line up perfectly.

IsAllOfAKind<T, TUnion> receives an array type and a string union, and returns true only when the array contains exactly one element of each literal type in the union. No member may be missing, no member may appear twice, and no stranger may sneak in. The interesting part is that the type system gives you no set operations to lean on: you cannot ask a union for its size, and you cannot iterate it in order. You have to turn the question into something the conditional type machinery can actually answer.

type A = IsAllOfAKind<['one', 'two'], 'one' | 'two'> // expected to be true
type B = IsAllOfAKind<['one', 'one'], 'one'> // expected to be false

Challenge Instructions: All of a Kind

Hard

Implement the IsAllOfAKind type. It receives an array type and a string union and determines whether or not the given array type includes exactly one element of each literal type of the string union.

View on GitHub: https://tsch.js.org/37233

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type IsAllOfAKind<T extends readonly string[], TUnion extends string> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type cases = [
  Expect<Equal<IsAllOfAKind<['one'], 'one'>, true>>,
  Expect<Equal<IsAllOfAKind<['one', 'one'], 'one'>, false>>,
  Expect<Equal<IsAllOfAKind<[], 'one'>, false>>,
  Expect<Equal<IsAllOfAKind<['one'], 'two'>, false>>,
  Expect<Equal<IsAllOfAKind<['one', 'two'], 'one' | 'two'>, true>>,
]

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

One-time payment, lifetime access. 30-day money-back guarantee.

Detailed Explanation

The solution in full:

type IsAllOfAKind<
  T extends readonly string[],
  TUnion extends string,
> = T extends readonly [
  infer Head extends string,
  ...infer Rest extends readonly string[],
]
  ? Head extends TUnion
    ? IsAllOfAKind<Rest, Exclude<TUnion, Head>>
    : false
  : [TUnion] extends [never]
    ? true
    : false

The whole idea fits in one sentence: treat the union as a shopping list and tick items off it as you walk the tuple.

Destructuring the tuple

T extends readonly [infer Head extends string, ...infer Rest extends readonly string[]] splits the tuple into its first element and the remainder. The infer X extends Y form is what lets the recursion keep working: without the constraints, Head would be unknown and Rest would be unknown[], neither of which satisfies the parameters of IsAllOfAKind.

For T = ['one', 'two'] the pattern evaluates to:

// Head = 'one'
// Rest = ['two']

When T is empty the pattern does not match at all, and we fall through to the base case. That is deliberate: the empty tuple is where the verdict gets made.

Exclude as a shrinking checklist

Head extends TUnion asks whether the current element is still wanted. If it is not, we are done and the answer is false. If it is, we recurse with Exclude<TUnion, Head>, which is the same union minus the member we just consumed.

Shrinking the union is what makes duplicates impossible. The second 'one' in ['one', 'one'] is tested against a union that no longer contains 'one', so it fails the same check a stranger would fail. One pass over the tuple catches both error kinds without ever comparing elements to each other.

Following IsAllOfAKind<['one', 'two'], 'one' | 'two'> step by step:

// step 1: Head = 'one', TUnion = 'one' | 'two' -> recurse with 'two'
// step 2: Head = 'two', TUnion = 'two'         -> recurse with never
// step 3: T = [], TUnion = never               -> true

Why the base case needs square brackets

Once the tuple runs out, the leftover union tells us whether anything was missed. An empty leftover means every member was matched exactly once.

The check is written as [TUnion] extends [never] rather than TUnion extends never, because a naked type parameter in a conditional type distributes over unions, and distributing over never produces never instead of running either branch. The result would be never where true belongs. Wrapping both sides in a one element tuple turns off distribution and compares the types as they are.

Edge cases the tests cover

The pattern generalizes well. Any time you need to compare a union against a sequence, converting the union into a value that shrinks as you consume it is usually easier than trying to count it.

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges