【问题标题】:Typescript no repeat value打字稿没有重复值
【发布时间】:2021-10-01 12:49:57
【问题描述】:

有没有办法用 uniq 值声明类型数组,

我有一系列字符串文字,如果我可以说每个字符串文字只能有一个,那我会很好。

export type AllMapLibraries = 'drawing' | 'geometry' | 'places' | 'visualization';

export const getGoogleApiUrl = (apiKey: string, libraries: AllMapLibraries[]) => {
  const librariesString = libraries.join(',');
  const url = `https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=${librariesString}&key=${apiKey}`;
  return url;
};

const url = getGoogleApiUrl('123', ['drawing', 'geometry', 'drawing']); // here should be error that drawing was already defined;

它不仅是数组。

但我什么都没找到,认为这是不可能的。

这是playground

【问题讨论】:

  • 也许您可以将libraries 声明为集合而不是数组?我不确定是否有一种类型可以模拟数据结构的作用。

标签: typescript


【解决方案1】:

如果你真的想在类型系统中表示“没有重复元素的数组”,你最好的选择是作为generic 类型,充当constraint。下面是我的开始:

type RemoveArrayRepeats<T extends readonly any[]> = {
    [K in keyof T]: (
        T[number] extends { [P in keyof T]: P extends K ? never : T[P] }[number]
        ? never
        : T[K]
    )
}

因此,RemoveArrayRepeats 类型采用 tuple 类型并将其转换为自身的版本,其中任何重复的元素都将替换为 never 类型。像这样:

type TestSame = RemoveArrayRepeats<[1, 2, 3, 4, 5]>; // [1, 2, 3, 4, 5]
type TestDiff = RemoveArrayRepeats<[1, 2, 3, 2, 5]>; // [1, never, 3, never, 5]

它是如何工作的草图:给定一个元组类型T,它maps over每个类似数字的索引K,并将T(即T[number])的元素类型的并集与T 的元素类型的联合,没有索引 K 处的元素(即 {[P in keyof T]: P extends K ? never: T[P]}[number].)。如果它们不同,那就太好了,不要管这个属性。如果它们相同,我们有一个重复...替换为never

[1,2,3,2,5]为例:如果K"0",那么我们比较[1,2,3,2,5]的元素联合,即1|2|3|5[never,2,3,2,5]的联合,即2|3|5。由于它们不同,因此元组的第一个元素不是重复的,它被单独留下:1。现在我们看K"1"。我们比较1|2|3|5[1,never,3,2,5]的元素,仍然是1|2|3|5。它们是一样的,意味着元组的第二个元素将被替换为never。最终我们建立了数组[1,never,3,never,5]


接下来我们将getGoogleApiUrl 函数声明为泛型函数,其中libraries 参数被限制为AllMapLibraries[]RemoveArrayRepeats&lt;T&gt;

declare const getGoogleApiUrl: <T extends AllMapLibraries[]>(
    apiKey: string,
    libraries: (T & RemoveArrayRepeats<T>) | []
) => string;

约束如何发生的细节有点奇怪。我选择将T &amp; RemoveArrayRepeats&lt;T&gt; 放在参数位置,这样T 本身只会被限制为AllMapLibraries[];这样可以使错误消息(例如'drawink')保持可以理解。但是您可以改为使用T extends AllMapLibraries[] &amp; RemoveArrayRepeats&lt;T&gt; 来获得非常相似的行为。

哦,libraries 类型中的| [] 是一个trick,它给编译器一个提示,如果可能的话,我们想为libraries 推断一个元组类型。没有它,编译器会将["drawing", "drawing", "drawing"] 视为Array&lt;"drawing"&gt; 类型,这将完全丢失我们检测重复所需的信息。


让我们看看它是否有效:

getGoogleApiUrl('', []); // okay
getGoogleApiUrl('', ['drawing']); // okay
getGoogleApiUrl('', ['drawing', 'geometry']); // okay
getGoogleApiUrl('', ['drawing', 'geometry', 'places', 'visualization']); // okay
getGoogleApiUrl('', ['foo']); // error!
//                   ~~~~~
// Type '"foo"' is not assignable to type 'AllMapLibraries'


getGoogleApiUrl('123', ['drawing', 'geometry', 'drawing']); // error!
//                      ~~~~~~~~~              ~~~~~~~~~
// Type 'string' is not assignable to type 'undefined'

这一切都或多或少符合预期。最后一种情况下的错误消息(如“string is not undefined”)不是您在完美世界中可能希望看到的。如果有支持的custom invalid type,我们将使用它来定制错误消息。但是没有。

至少红色波浪线位于有意义的位置。好吧,也许您只希望其中 一个 是错误的;但这会使RemoveArrayRepeats 更加复杂(例如,仅将一个元素与较低索引的元素进行比较,但编译器不理解“较低的索引”,因此我们需要一些表示,它会很快变得丑陋)。

但这是我可以在没有代码页的情况下最接近您的要求。这个版本是否值得由你决定。无论如何,希望有所帮助;祝你好运!

Link to code

【讨论】:

    【解决方案2】:

    您可以使用设置!

    var uniques = new Set([0,1,1,2])
    

    这会导致:

    Set(3) {0, 1, 2}
    

    然后添加新元素,例如:

    uniques.add(1)
    

    还会导致:

    Set(3) {0, 1, 2}
    

    【讨论】:

    • yes set 将在运行时完成这项工作,但我想在编译之前在 ts 中已经有错误
    • @JurajKocan 在编译时仅使用类型系统进行唯一性检查是奇怪。这可能是可能的,但是定义必须是递归的。如果你这样做libraries.add("places"),会发生什么会很奇怪。如果你想使用类型来解决这个问题,使用{drawing: boolean, geometry: boolean, places: boolean, visualization: boolean} 对象似乎更合适,它将保证你没有重复项。否则,通常会在运行时执行 Set 或简单地过滤函数中的重复项。
    • @VLAZ 的重点是我想要有好的代码,有像你说的 set 或 object 这样的解决方案,或者只是在 func 中命名这 4 个参数。但我正在寻找解决方案,我只需调用这个函数,如 getGoogleApiUrl('apikey', ''lib1, lib2'') (或 ['lib1', 'lib2', 'lib3'])
    • 那么一个“不错的代码解决方案”似乎是使用一个选项对象和可能的库作为键。对我来说,这比发送字符串数组更有意义。验证 fn([1, 2])fn([1, 2, 1]) 可能会很好,但您始终可以传递一个数组变量,所以如果您执行 myArr = [1]; /* later */ myArr.push(2); /* later */ myarr.sort(sorter); /* later */ myArr.splice(1, 1); /* later */ fn(myArr.concat(otherVar)) 会发生什么 - 如果此代码需要是类型安全的。那将如何运作?需要像在运行时一样评估操作,但没有运行时数据的好处。
    • 事实上我并不关心运行时,如果你做 ['drawing', 'drawing', 'drawing'] 然后将它发送到谷歌 api 它无论如何都会工作。所以我什至不将在运行时验证这些值。即使您发送没有现有库的请求。所以这一切只是为了预编译时间和“漂亮的代码”、“易于编写”
    【解决方案3】:

    如果您需要编译时检查,您有几个选择。

    1. 使用具有内置重复错误的对象。
    type AllMapLibraries = 'drawing'|'geometry';
    type AllMapLibraries_Obj = {[P in AllMapLibraries]:boolean};
    const getGoogleApiUrl = (apiKey:string, librariesObj:AllMapLibraries_Obj) => {
        const libraries = Object.keys(librariesObj) as AllMapLibraries[];
    
        // alternatively you could do
        if ('geometry' in librariesObj) {}
    
        // ...
    };
    
    // This will error, you can't specify the same object property twice
    const url = getGoogleApiUrl('123', { drawing: true, drawing: true });
    
    // This will work
    const url = getGoogleApiUrl('123', { geometry: true });
    
    1. 使用具有基于索引的约束的元组。
    type AllMapLibraries = Partial<['drawing', 'geometry']>;
    const getGoogleApiUrl = (apiKey:string, libraries:AllMapLibraries) => {
        // ...
    };
    
    // This will error, you can't use 'drawing' in the index that 'geometry' goes
    const url = getGoogleApiUrl('123', ['drawing', 'drawing']);
    
    // This will work
    const url = getGoogleApiUrl('123', [,'geometry']);
    

    缺点:

    对象可能更烦人,可能需要大量重写。

    如果你有一个大数组,元组很难使用,因为你可能需要很多逗号才能找到正确的索引。您还需要过滤掉undefined

    【讨论】:

      猜你喜欢
      • 2022-11-22
      • 2020-01-08
      • 2020-09-21
      • 1970-01-01
      • 2016-09-20
      • 1970-01-01
      • 1970-01-01
      • 2017-04-30
      • 1970-01-01
      相关资源
      最近更新 更多