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

vd2z7a6w  于 2022-12-19  发布在  TypeScript
关注(0)|答案(7)|浏览(176)

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

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

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

PS:我目前正在使用JS删除重复的项目,但是,好奇的是,如果这个错误可以捕获使用 typescript 类型之前的手?

fykwrbwg

fykwrbwg1#

在编译时,唯一可行的方法是数组是由文本组成的元组。例如,下面是一些运行时值相同但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[],那么答案就是"不,你不能这么做"。
如果你正在从一个使用你的代码作为库的开发者那里得到一个常量元组(可能通过constAssert),那么你有机会得到一些有用的东西,但是它很复杂,而且可能很脆弱。
首先,我们提出了一个类似invalid type的东西,TypeScript没有这个东西。这个想法是一个不能赋值的类型(比如never),但是当编译器遇到它的时候,它会产生一个自定义的错误消息。下面的代码并不完美,但是如果你仔细看的话,它产生的错误消息可能是合理的:

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

现在我们表示UniqueArray,它不能作为一个具体的类型(所以没有const a: UniqueArray = ...)但是我们可以把它表示为一个泛型约束,传递给一个helper函数。不管怎样,这里有AsUniqueArray<A>,它接受一个候选数组类型A,如果它是唯一的,则返回A。否则返回一个不同的数组,其中重复的位置有错误消息:

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

它使用了大量的Map类型和条件类型,但是它实际上遍历了数组,并查看数组中是否有其他元素与当前元素匹配,如果匹配,则会显示一条错误消息。
现在来看看helper函数,另一个问题是,默认情况下,像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"]>

无论如何,所有这些对你来说可能不值得,但是我想展示有一种,一种,也许,一种方法可以接近类型系统。希望能有所帮助;祝你好运!
Playground代码链接

f5emj3cl

f5emj3cl2#

是的!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.
在TypeScriptPlayground试用

j2qf4p5b

j2qf4p5b3#

approved answer非常相似,但是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"]

打字机游戏场

qc6wkl3g

qc6wkl3g4#

又一个可读的(我希望)解决方案。

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

TSPlayground

yk9xbfzb

yk9xbfzb5#

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

wxclj1h5

wxclj1h56#

适用于需要简单解决方案的用户的变通方案:
如果创建一个对象,重复的键将抛出如下错误:

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

但问题是,键是字符串,除了字符串,你不能在键中保存任何东西。

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

如果你不想拥有一个同时包含数字和字符串的唯一数组,这可能对你有用。然后,你可以像这样使用该对象:

Object.keys(anObject)

,并将其用作数组。

2cmtqfgy

2cmtqfgy7#

Typescript只引用类型,不引用值,而且,在运行时数组上它什么也不做。

相关问题