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

时间:2019-07-13 06:29:20

标签: typescript types typescript-typings typescript-types

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

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

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

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

4 个答案:

答案 0 :(得分:3)

是的! TypeScript 4.1有一种方法(在撰写本文时为beta)。这是这样的:

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

答案 1 :(得分:1)

Typescript仅在类型上引用,而不在值上引用,而且在运行时数组上将不执行任何操作。

答案 2 :(得分:1)

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

答案 3 :(得分:1)

在编译时唯一可行的方法是,如果您的数组是由tuples组成的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];

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


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

首先,我们提出一种类似于invalid type的行为,而TypeScript则没有。这个主意是一种不能分配任何值的类型(例如never),但是当编译器遇到该错误类型时会生成自定义错误消息。以下内容并非十全十美,但是如果您斜视,它会产生可能合理的错误消息:

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

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

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

这使用了大量的mappedconditional类型,但实际上它遍历了数组,并查看该数组中是否还有其他元素与当前元素匹配。如果是这样,则会出现错误消息。

现在使用辅助功能。另一个难题是,默认情况下,像doSomething([1,2,3])这样的函数会将[1,2,3]视为number[]而不是文字的[1,2,3]元组。没有simple way to deal with this,因此我们必须使用奇怪的魔术(请参阅有关该魔术的讨论链接):

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

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

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

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

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

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

Link to code