#30178Hard

Unique Items

Write uniqueItems, a function that rejects tuples with duplicate elements and pins the compiler error on each repeated element instead of the whole argument.

Make the compiler your linter: write a function that accepts [1, 2, 3] but red-squiggles the second 2 in [1, 2, 2], on exactly that element.

The tool for this is a constrained identity function (CIF): a function that returns its argument unchanged but uses its type signature to enforce a rule that no plain type annotation could express. Here the rule is that all elements of a tuple are unique. Some concepts in TypeScript can't be described by types, but can be expressed through type constraints. For example, you can't define a type for positive numbers, but you can check whether a number literal type is positive:

// Ensures `n` is a positive number
function positive<const N extends number>(n: `${N}` extends `-${string}` ? never : N) {
  return n
}
 
const a = positive(1) // Ok
const b = positive(-1) // Error, -1 is not assignable to never

Your task is a CIF uniqueItems that takes a tuple of literals and ensures all of them are unique. There are two bonus goals: helpful error messages instead of not assignable to never, and errors that land on the individual repeated elements rather than the whole argument. Along the way you'll use const type parameters, tuple recursion with a "seen" accumulator, and self-referential parameter types.

uniqueItems([1, 2, 3]) // OK, returns readonly [1, 2, 3]
 
uniqueItems([
  1,
  2,
  2, // Error here: Type '2' is not assignable to type '2 & DuplicateItemError<2>'
  3,
])

Challenge Instructions: Unique Items

Hard

Some concepts in TypeScript can not be described by types, but can be expressed through type constraints. For example, you can't define a type for positive numbers, but you can check whether a number literal type is positive. One of the patterns for applying such constraints is constrained identity function (CIF). A CIF takes one parameter, infers its type, performs additional checks and returns the parameter unmodified.

// Ensures `n` is a positive number
function positive<const N extends number>(n: `${N}` extends `-${string}` ? never : N) {
return n
}
 
const a = positive(1) // Ok
const b = positive(-1) // Error, -1 is not assignable to never

Write a CIF uniqueItems that takes a tuple of literals and ensures that all of them are unique. You are free to use either mutable or readonly tuples.

Bonus task: Helpful error messages instead of not assignable to never. Bonus task: Only repeating tuple elements should be treated as errors, not the entire argument.

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

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

👋 Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The solution in full:

interface DuplicateItemError<Item> {
  error: 'Duplicate item'
  item: Item
}
 
type Includes<Haystack extends readonly unknown[], Needle> =
  Haystack extends readonly [infer Head, ...infer Rest]
    ? Equal<Head, Needle> extends true
      ? true
      : Includes<Rest, Needle>
    : false
 
type UniqueItems<
  Items extends readonly unknown[],
  Seen extends readonly unknown[] = [],
> = Items extends readonly [infer Head, ...infer Rest]
  ? readonly [
      Includes<Seen, Head> extends true ? DuplicateItemError<Head> : Head,
      ...UniqueItems<Rest, [...Seen, Head]>,
    ]
  : readonly []
 
function uniqueItems<const Items extends readonly unknown[]>(
  items: Items & UniqueItems<Items>,
): Items {
  return items
}

The core idea: infer the exact tuple the caller passed, rebuild a "corrected" version of it where every duplicate element is replaced by an error type, and demand that the argument match that corrected version. Where the tuples agree, nothing happens. Where they disagree, the compiler complains on that exact element.

Capturing literals with const Items

Without help, TypeScript widens [1, 2, 2] to number[], and once the literals are gone there's nothing left to compare. The const modifier on the type parameter (TypeScript 5.0+) tells the compiler to infer the argument as if it had as const: uniqueItems([1, 2, 2]) infers Items as readonly [1, 2, 2], an exact tuple of literal types. That's the raw material every later step depends on.

Includes: has this element appeared before?

Includes<Haystack, Needle> is a type-level Array.prototype.includes. It walks the tuple one head at a time and compares each element to the needle with Equal, the strict comparison type that ships with the challenge's helpers, rather than with extends.

What is Equal? It's defined as type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false. It embeds X and Y in two otherwise identical generic function signatures and asks whether those signatures are assignable. The compiler only answers true when the two types are identical, not merely mutually assignable. That's what makes it stricter than any extends check.

That distinction matters with mixed literals: Equal<false, boolean> is false, while an extends check would happily say false is a boolean and report phantom duplicates. A quick intermediate example:

type A = Includes<[1, 'a'], 'a'> // true
type B = Includes<[1, 'a'], true> // false

UniqueItems: rebuilding the tuple with errors baked in

This is the heart of the solution. It recurses through Items while carrying a Seen accumulator of everything already visited, and emits one output element per input element:

For Items = [1, 2, 2] the result evaluates to:

[object Object]

Two details are worth pausing on. First, only the second and later occurrences get replaced. The first 2 was legitimately new when it appeared, which is exactly what the bonus task asks for. Second, DuplicateItemError is a plain interface with an error message and the offending item. It exists to be incompatible with the real element and to show up verbatim in the compiler output: instead of the cryptic not assignable to never you read Type '2' is not assignable to type '2 & DuplicateItemError<2>', and the error text tells you what went wrong.

The intersection trick: Items & UniqueItems<Items>

This is the "self-referential parameter type" promised in the intro: the parameter's type is written in terms of Items, the very type parameter being inferred from that same parameter. Why not just declare items: UniqueItems<Items>? Because then Items would appear only inside a conditional type, and TypeScript would have no position to infer it from. Inference needs a "naked" Items somewhere in the parameter, and the intersection gives it one: the compiler infers Items from the left half, then checks the argument against both halves.

Crucially, when TypeScript checks an array literal against a tuple type it elaborates mismatches element by element, attaching each error to the element expression itself. That's what makes the second bonus work: in a multi-line call, the squiggle appears on the line of the repeated element, not on the whole argument.

Edge cases the tests cover

CIFs are a pattern worth keeping in your toolbox: any rule you can check about a literal type, you can enforce at a call site, without a line of runtime code.

This challenge is originally from here.

Share this challenge

Learn the Concepts