有没有办法在打字稿中为具有唯一项的数组定义类型?

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

类型应该检测数组是否有重复项并在打字稿中抛出错误?

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

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

PS:我目前正在使用 JS 删除重复项目,但是,好奇是否可以使用打字稿类型事先捕获此错误?

Typescript 只引用类型,不引用值,此外,在运行时数组上它什么都不做。

Typescript 仅执行编译时检查。 Typescript 无法检测到在运行时修改数组。 另一方面,您可能希望使用 Set class 来防止插入重复项(但不会引发错误,除非您检查 return 值)。但这不会引发编译时错误。

如果您的数组是 tuples composed of literals,这在编译时唯一可行的方法是。例如,这里有一些数组具有相同的运行时值,但在 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;

只有第一个会如您所愿...编译器会意识到 tupleOfLiterals 恰好有 3 个元素,其中两个是同一类型。在所有其他情况下,编译器不了解发生了什么。因此,如果您要传递从其他函数或 API 等获得的数组,并且这些数组的类型类似于 number[],那么答案就是“不,您可以不要这样做”。


如果您正在获取文字元组(可能通过 const assertion)...比如说,从使用您的代码作为库的开发人员那里,您就有机会获得一些有用的东西,但它是复杂且可能易碎。我可能会这样做:

首先我们想出一些类似于 invalid type, which TypeScript doesn't have. The idea is a type to which no value can be assigned (like never) 的东西,但是当编译器遇到它时会产生自定义错误消息。以下内容并不完美,但它会产生错误消息,如果您眯着眼睛看,这些消息可能是合理的:

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

现在我们代表UniqueArray。它不能作为具体类型完成(所以没有 const a: UniqueArray = ...),但我们 可以 将其表示为我们传递给辅助函数的 generic constraint。无论如何,这里的 AsUniqueArray<A> 需要一个候选数组类型 A 和 returns A 如果它是唯一的,否则 returns 一个不同的数组,那里有错误消息在重复的地方:

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]
};

它使用了很多 mapped and conditional 类型,但它实际上遍历了数组并查看数组中是否有任何其他元素与当前元素匹配。如果是这样,则会出现错误消息。

现在是帮助函数。另一个问题是,默认情况下,像 doSomething([1,2,3]) 这样的函数会将 [1,2,3] 视为 number[] 而不是 [1,2,3] 文字元组。没有simple way to deal with this,所以我们不得不使用奇怪的魔法(关于那个魔法的讨论见link):

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

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

现在,asUniqueArray() 只是 returns 它在运行时的输入,但在编译时它只会接受它认为唯一的数组类型,如果有问题的元素,它就会出错重复:

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

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

万岁,这就是你想要的,对吧?一开始的注意事项仍然有效,所以如果您最终得到的数组已经被加宽(non-tuples 或 non-literals),您将有不良行为:

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"]>

无论如何,所有这些对您来说可能不值得,但我想表明,有一种,某种,也许,一种方法可以通过类型系统接近这一点。希望有所帮助;祝你好运!

Playground link to code

是的! TypeScript 4.1 有一种方法(在撰写本文时处于测试阶段)。是这样的:

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

如果相同的值多次出现,您将收到编译器错误。

Here's my article describing things in better detail.

Try in TypeScript Playground

非常相似,但 InArray 进行了简化和内联。

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