【问题标题】:TypeScript: Require that two arrays be the same length?TypeScript:要求两个数组的长度相同?
【发布时间】:2020-09-24 03:04:00
【问题描述】:

假设我正在制作一个名为 TabsPanelsTab + Panel 组件。我想确保获得相同数量的 TabPanel 组件,如下所示:

type TabsPanelsProps = {
  tabs: Tab[];
  panels: Panel[];
}
<TabsPanels
  tabs={[<Tab/>, <Tab/>]}
  panels={[<Panel/>]} // Error: tabs.length and panels.length do not match
/>

有没有办法做到这一点?如果有一些实用功能,比如

PropsAreEqual<T, K1, K2, P>
where
T = type
K1 = key 1
K2 = key 2
P = the property to be equal

这显然很糟糕,但你明白我在说什么。那你就可以了

PropsAreEqual<TabsPanelsProps, 'tabs', 'panels', 'length'>

【问题讨论】:

  • 如果您期望一对一的选项卡与面板配对,那么您的数据结构确实应该是自然支持的东西(例如,[Tab, Panel] 的元组如下 Daniel 建议的或一种新类型这需要提供单独的选项卡和面板字段,即{ readonly tab: Tab, readonly panel: Panel })。
  • 这是一个很好的观点,这也是我最终要做的。不过还是对这里的一些答案很感兴趣!

标签: javascript reactjs typescript


【解决方案1】:

我看到一些关于推断数组文字长度的问题的答案。问题在于,当您将数组文字传递给函数时,编译器通常会将其扩展为数组并且不会将其解释为固定长度的元组。这通常是您想要的;数组经常改变长度。当您希望编译器将[1, 2] 视为一对而不是数组时,您可以给编译器一个提示:

function requireTwoSameLengthArrays<
    T extends readonly [] | readonly any[]
>(t: T, u: { [K in keyof T]: any }): void { }

请注意,泛型类型参数Tgeneric constraint 是空元组类型[] 和数组类型any[] 的联合。 (不要担心readonly;这个修饰符使函数更通用,而不是更具体,因为string[] 可分配给readonly string[],反之亦然。)在联合中拥有空元组类型不会改变T 可以是 种类 的事物(毕竟,any[] 已经包含空元组 [])。但它确实给编译器一个提示需要元组类型。

因此编译器会将[1, 2] 推断为[number, number] 而不是number[]


检查上面的签名,您会看到u 参数是mapped array/tuple type。如果T 是一个元组,则{[K in keyof T]: any} 是一个与T 长度相同的元组。

让我们看看它的实际效果:

requireTwoSameLengthArrays([1, 2], [3, 4]); // okay
requireTwoSameLengthArrays([1, 2], [3]); // error! property 1 is missing in [number]!
requireTwoSameLengthArrays([1, 2], [3, 4, 5]); // error! length is incompatible!

万岁!


请注意,如果编译器已经忘记了元组的长度,这将不起作用:

const oops = [1, 2]; // number[]
requireTwoSameLengthArrays(oops, [1, 2, 3]); // okay because both are of unknown length

oops 的类型被推断为number[],并将其传递给requireTwoSameLengthArrays() 无法撤消该推断。太晚了。如果您希望编译器只拒绝长度完全未知的数组,您可以这样做:

function requireTwoSameLengthTuples<
    T extends (readonly [] | readonly any[]) & (
        number extends T["length"] ? readonly [] : unknown
    )>(t: T, u: { [K in keyof T]: any }): void { }

这更难看,但它所做的是检查T 的长度是否为number 而不是某些特定的numeric literal。如果是这样,它会通过要求一个空元组来阻止匹配。这有点奇怪,但它有效:

requireTwoSameLengthTuples([1, 2], [3, 4]); // okay
requireTwoSameLengthTuples([1, 2], [3]); // error! [number] not [any, any]
requireTwoSameLengthTuples([1, 2], [3, 4, 5]); // error! ]number, number, number]

requireTwoSameLengthTuples(oops, [1, 2, 3]); // error on oops!
// Types of property 'length' are incompatible.

好的,希望对您有所帮助;祝你好运!

Playground link to code

【讨论】:

  • 现在这真是太棒了。谢谢!
  • 在任何地方都记录了使用添加只读 [] 的技巧吗?我想知道如果我没有遇到这个答案,我还能在哪里找到它。
【解决方案2】:

这可以通过要求消费者将一个长度泛型类型传递给函数来实现

function same<T extends number>(
  nums: (readonly number[] & { readonly length: T }),
  strings: (readonly string[] & { readonly length: T })
) { }

same<2>(
  [3, 4] as const,
  ['1', '4'] as const
)

唯一的限制是您需要传入&lt;2&gt;,否则打字稿足以推断N 的通用number,您还需要声明所有参数as const 以使其丢失通过类型擦除的元组长度

要让它在反应渲染函数中使用,你需要做一些额外的一些丑陋的 TypeScript 条件类型

【讨论】:

【解决方案3】:

以下是部分答案:

type ArrayOfFixedLength<T extends any, N extends number> = readonly T[] & { length: N }; 

const a1: ArrayOfFixedLength<number, 2> = [1] as const; 
const a2: ArrayOfFixedLength<number, 2> = [1, 2] as const; 


function myFunction<N extends number>(array1: ArrayOfFixedLength<any, N >, array2: ArrayOfFixedLength<any, N>) {
return true; 
}

myFunction<3>([1, 2, 3] as const, [2, 3, 4] as const); 
myFunction<2>([1, 2] as const, [1, 2, 3] as const);

// However, if you don't specify the array length, 
// It fails to error
myFunction([1, 2, 3] as const, [2, 3, 4] as const); 
myFunction([1, 2] as const, [1, 2, 3] as const);

playground

说明

因为我们本质上是使用数组的length 属性来检查它们是否相等,所以需要将数组视为不可变的,因此使用readonlyas const 关键字。

我们所做的是声明我们有一个数组,并且这个数组的长度总是 N。

现在,我对这段代码的不理解是为什么需要myFunction&lt;3&gt; 中的通用参数。我不知道为什么 N 的值不是由传入的参数推断出来的。

现在,在您的情况下,您最好只在运行时检查数组长度并抛出错误。请记住,打字稿是为了开发人员的经验,通常是在错误发生之前捕获错误,但在这种情况下,执行运行时错误可能是捕获此类错误的最快和最简单的方法。

【讨论】:

【解决方案4】:

@jcalz answerinspiration from @cancerbero 上进行迭代,我发现了一个稍微不同的策略,在我的情况下效果很好:

type Tuple<T> = readonly T[] | readonly [];
type TupleOfLength<T, Length extends number> = Length extends 0 ? readonly [] :
    readonly [T, ...T[]] & { readonly length: Length };

type LengthOfTuple<Tuple extends readonly any[]> = number extends Tuple['length'] ? never : Tuple['length'];

function requireTwoSameLengthTuples<T, FirstArray extends Tuple<T>>(
    firstArray: FirstArray,
    secondArray: TupleOfLength<T, LengthOfTuple<FirstArray>>
) {
    // Do your processing
}

requireTwoSameLengthTuples([1, 2], []); // fails
requireTwoSameLengthTuples([1, 2], [3]); // fails
requireTwoSameLengthTuples([1], [2, 3]); // fails
requireTwoSameLengthTuples([1, 2], [3, 4]); // works

const array1 = [1, 2];
const array2 = [3, 4];
requireTwoSameLengthTuples(array1, array2); // fails too

Typescript playground

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-29
    相关资源
    最近更新 更多