Ganapati V S
Ganapati V S

Reputation: 1661

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

The type should detect if the array has duplicate items and throw error in typescript?

type UniqueArray = [
  // How to implement this?
]

const a:UniqueArray = [1, 2, 3] // success
const b:UniqueArray = [1, 2, 2] // error

PS: I'am currently removing duplicate items using JS, but, curious if this error can be captured using typescript type before hand?

Upvotes: 35

Views: 22564

Answers (8)

ScreamZ
ScreamZ

Reputation: 602

What I do currently is wrap my function argument to be a JavaScript Set, then i use values with Array.from(mySet).

function someFn(map: List, keysSet: Set<keyof List>) {
  const uniqueValue = Array.from(keysSet);
}

This way even if you're duplicating, it shouldn't be an issue.

Upvotes: 0

vf1
vf1

Reputation: 11

Yet another readable (I hope) solution.

type ExcludeByIndex<T extends readonly unknown[], I extends keyof T & (number | string)> = {
    [X in keyof T]: X extends (`${I}` | I) ? never : T[X]
}

export type ExcludeNotUnique<T extends readonly unknown[]> = {
    [I in keyof T]-?: T[I] extends ExcludeByIndex<T, I>[number] ? never : T[I]
}


const ids = [1, 2, 2, 3, 4, 5] as const;

type UniqueIds = ExcludeNotUnique<typeof ids>;

function DummyUniqueIdsTest(): UniqueIds[number] {
    return ids[Math.random()];
}

ts playground

Upvotes: 1

Sahin
Sahin

Reputation: 1383

A workaround for the people who wants a simple solution:
If you create an object, a duplicate key will throw error like:

const anObject = {
    1: 0,
    2: 0,
    2: 0, //will throw error
}

But the problem is, keys are string, you can't save anything in the keys other than string.

const anObject = {
    1: 0,
    2: 0,
    "2": 0, //will throw error
    "foo": 0
}

If you are not trying to have a unique array with both number and strings, this might work for you. And then, you can use the object like:

Object.keys(anObject)

, and use it as array.

Upvotes: -3

Tirondzo
Tirondzo

Reputation: 176

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

Upvotes: 6

jcalz
jcalz

Reputation: 328292

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

Upvotes: 33

Jan Sommer
Jan Sommer

Reputation: 3798

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

Upvotes: 31

SOFe
SOFe

Reputation: 8214

Typescript only performs compile time inspection. Modifying the array at runtime cannot be detected by Typescript. On the other hand, you might want to use the Set class to prevent duplicate items from being inserted (but won't raise errors unless you check return value). But that won't raise compile time errors.

Upvotes: -2

VitoMakarevich
VitoMakarevich

Reputation: 455

Typescript refers only on type, not value, moreover, on runtime arrays it will do nothing.

Upvotes: -7

Related Questions