有没有一种方法可以为打字稿中具有唯一项的数组定义类型?

Gan*_*V S 2 types typescript typescript-typings typescript-types

类型应该检测数组是否有重复的项目并在打字稿中引发错误?

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

const a:UniqueArray = [1, 2, 3] // success
const b:UniqueArray = [1, 2, 2] // error
Run Code Online (Sandbox Code Playgroud)

PS:我目前正在使用JS删除重复项,但是,好奇是否可以使用手写脚本类型捕获此错误?

Jan*_*mer 8

是的!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
Run Code Online (Sandbox Code Playgroud)

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

这是我的文章,更详细地描述了事情。

在 TypeScript Playground 中尝试

  • @jcalz,当 TypeScript 贡献者说存在浅递归限制时,我倾向于相信这确实是我答案的限制。 (2认同)

jca*_*alz 6

这可能在编译时起作用的唯一方法是,如果数组是由文字组成的元组。例如,以下一些数组具有相同的运行时值,但在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];
Run Code Online (Sandbox Code Playgroud)

只有第一个tupleOfLiterals元素会表现出您想要的行为……编译器会意识到它恰好具有3个元素,其中两个是同一类型。在所有其他情况下,编译器都不了解发生了什么。因此,如果要传递的数组是从其他函数或API等获得的,而这些数组的类型类似于number[],则答案只是“不,您不能这样做”。


如果您正在获取文字的元组...比如说,从开发人员那里使用您的代码作为库,那么您就有机会获得一些有用的东西,但是它很复杂并且可能很脆弱。这是我可能的方法:

首先,我们提出一种类似于无效类型的东西,而TypeScript没有。这个想法是一种类型,不能给它赋值(例如never),但是当编译器遇到它时会产生一个自定义错误消息。以下内容并非十全十美,但是如果您斜视,它会产生可能合理的错误消息:

type Invalid<T> = Error & { __errorMessage: T };
Run Code Online (Sandbox Code Playgroud)

现在我们代表UniqueArray。它不能作为具体类型来完成(所以no const a: UniqueArray = ...),但是我们可以将其表示为传递给辅助函数的一般约束。无论如何,AsUniqueArray<A>这是一个候选数组类型AA如果唯一,则返回,否则返回另一个数组,该数组中重复出现错误消息的地方:

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]
};
Run Code Online (Sandbox Code Playgroud)

这使用了许多映射条件类型,但实际上它遍历了数组,并查看数组中是否有其他元素与当前元素匹配。如果是这样,则出现错误信息。

现在为助手功能。另一个难题是,默认情况下,像这样的函数doSomething([1,2,3])将被[1,2,3]视为a number[]而不是[1,2,3]文字的元组。没有简单的方法可以解决这个问题,因此我们必须使用奇怪的魔术(请参阅有关该魔术的讨论链接):

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

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

现在,asUniqueArray()仅在运行时返回其输入,但是在编译时,它将仅接受它认为唯一的数组类型,并且如果存在重复,它将在问题元素上出现错误:

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

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

万岁,这就是您想要的,对吧?从一开始的警告仍然存在,因此,如果最终得到的数组已经被扩大了(非元组或非文字),则将出现不良行为:

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"]>
Run Code Online (Sandbox Code Playgroud)

无论如何,所有这些可能对您来说都不值得,但我想证明类型系统可以提供某种接近此的方法。希望能有所帮助;祝好运!

链接到代码


小智 6

与批准的答案非常相似,但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"]
Run Code Online (Sandbox Code Playgroud)

TypeScript 游乐场