Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
276 views
in Technique[技术] by (71.8m points)

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

1.4m articles

1.4m replys

5 comments

57.0k users

...