Is there a way to define type for array with unique items in typescript?

Yes! There is a way with TypeScript 4.1 (in beta at the time of writing). This is how:

const data = ["11", "test", "tes", "1", "testing"] as const
const uniqueData: UniqueArray<typeof data> = data

type UniqueArray<T> =
  T extends readonly [infer X, ...infer Rest]
    ? InArray<Rest, X> extends true
      ? ['Encountered value with duplicates:', X]
      : readonly [X, ...UniqueArray<Rest>]
    : T

type InArray<T, X> =
  T extends readonly [X, ...infer _Rest]
    ? true
    : T extends readonly [X]
      ? true
      : T extends readonly [infer _, ...infer Rest]
        ? InArray<Rest, X>
        : false

You'll get a compiler error if the same value occurs more than once.

Here's my article describing things in better detail.

Try in TypeScript Playground


Is very similar to the approved answer, but InArray is simplified and inlined.

type IsUnique<A extends readonly unknown[]> =
  A extends readonly [infer X, ...infer Rest]
    ? X extends Rest[number]
      ? [never, 'Encountered value with duplicates:', X] // false
      : IsUnique<Rest>
    : true;

type IsInArray<A extends readonly unknown[], X> = X extends A[number] ? true : false;
type TestA = IsUnique<["A","B","C"]>; // true
type TestB = IsUnique<["A","B","B"]>; // [never, "Encountered value with duplicates:", "B"]

TypeScript Playground


The only possible way this could work at compile time is if your arrays are tuples composed of literals. For example, here are some arrays with the same runtime values, but with different types in TypeScript:

const tupleOfLiterals: [1, 2, 2] = [1, 2, 2]; 
const tupleOfNonLiterals: [number, number, number] = [1, 2, 2];
const arrayOfLiterals: (1 | 2)[] = [1, 2, 2];
const arrayOfNonLiterals: number[] = [1, 2, 2];

const constAssertedReadOnlyTupleOfLiterals = [1, 2, 2] as const;

Only the first one would behave as you'd like... the compiler would realize that tupleOfLiterals has exactly 3 elements, two of which are the same type. In all the other cases, the compiler doesn't understand what's going on. So if you're passing arrays you get from other functions, or from an API, etc., and the type of these arrays is something like number[], then the answer is just "no, you can't do this".


If you're getting tuples of literals (possibly via const assertion)... say, from a developer using your code as a library, then you have a chance of getting something that works, but it's complex and possibly brittle. Here's how I might do it:

First we come up something that acts like an invalid type, which TypeScript doesn't have. The idea is a type to which no value can be assigned (like never) but which produces a custom error message when the compiler encounters it. The following isn't perfect, but it produces error messages that are possibly reasonable if you squint:

type Invalid<T> = Error & { __errorMessage: T };

Now we represent UniqueArray. It can't be done as a concrete type (so no const a: UniqueArray = ...) but we can represent it as a generic constraint that we pass to a helper function. Anyway, here's AsUniqueArray<A> which takes a candidate array type A and returns A if it is unique, and otherwise returns a different array where there are error messages in the places that are repeated:

type AsUniqueArray<
  A extends ReadonlyArray<any>,
  B extends ReadonlyArray<any>
> = {
  [I in keyof A]: unknown extends {
    [J in keyof B]: J extends I ? never : B[J] extends A[I] ? unknown : never
  }[number]
    ? Invalid<[A[I], "is repeated"]>
    : A[I]
};

That uses a lot of mapped and conditional types, but it essentially walks through the array and looks to see if any other elements of the array match the current one. If so, there's an error message.

Now for the helper function. Another wrinkle is that by default, a function like doSomething([1,2,3]) will treat [1,2,3] as a number[] and not a [1,2,3] tuple of literals. There's no simple way to deal with this, so we have to use weird magic (see the link for discussion of that magic):

type Narrowable =
  | string
  | number
  | boolean
  | object
  | null
  | undefined
  | symbol;

const asUniqueArray = <
  N extends Narrowable,
  A extends [] | ReadonlyArray<N> & AsUniqueArray<A, A>
>(
  a: A
) => a;

Now, asUniqueArray() just returns its input at runtime, but at compile time it will only accept array types it perceives as unique, and it will put errors on the problem elements if there are repeats:

const okay = asUniqueArray([1, 2, 3]); // okay

const notOkay = asUniqueArray([1, 2, 2]); // error!
//                                ~  ~
// number is not assignable to Invalid<[2, "is repeated"]> | undefined

Hooray, that is what you wanted, right? The caveats from the beginning still hold, so if you end up getting arrays that are already widened (either non-tuples or non-literals), you'll have undesirable behavior:

const generalArray: number[] = [1, 2, 2, 1, 2, 1, 2];
const doesntCareAboutGeneralArrays = asUniqueArray(generalArray); // no error

const arrayOfWideTypes: [number, number] = [1, 2];
const cannotSeeThatNumbersAreDifferent = asUniqueArray(arrayOfWideTypes); // error,
// Invalid<[number, "is repeated"]>

Anyway, all this might not be worth it for you, but I wanted to show that there is kind of, sort of, maybe, a way to get close to this with the type system. Hope that helps; good luck!

Playground link to code