Say I'm making a Tab
+ Panel
component called TabsPanels
. I want to ensure that I'm getting the same number of Tab
and Panel
components, like so:
type TabsPanelsProps = {
tabs: Tab[];
panels: Panel[];
}
<TabsPanels
tabs={[<Tab/>, <Tab/>]}
panels={[<Panel/>]} // Error: tabs.length and panels.length do not match
/>
Is there any way to do this? If there was some utility function like
PropsAreEqual<T, K1, K2, P>
where
T = type
K1 = key 1
K2 = key 2
P = the property to be equal
That's obviously bad, but you get what I'm saying. Then you could do
PropsAreEqual<TabsPanelsProps, 'tabs', 'panels', 'length'>
I see a few answers with questions around inferring the length of the array literals. The issue is that when you pass in an array literal to a function, the compiler generally widens it to an array and does not interpret it as a fixed-length tuple. This is often what you want; often arrays change length. When you want the compiler to see [1, 2]
as a pair and not as an array, you can give the compiler a hint:
function requireTwoSameLengthArrays<
T extends readonly [] | readonly any[]
>(t: T, u: { [K in keyof T]: any }): void { }
Notice that the generic type parameter T
's generic constraint is a union of an empty tuple type []
and an array type any[]
. (Don't worry about readonly
; this modifier makes the function more general, not more specific, since string[]
is assignable to readonly string[]
and not vice versa.) Having the empty tuple type in a union doesn't change the kinds of things that T
can be (after all, any[]
already includes the empty tuple []
). But it does give the compiler a hint that tuple types are desired.
So the compiler will infer [1, 2]
as [number, number]
instead of as number[]
.
Examining the signature above, you see that the u
argument is a mapped array/tuple type. If T
is an tuple, {[K in keyof T]: any}
is a tuple of the same length as T
.
So let's see it in action:
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!
Hooray!
Note that if the compiler has already forgotten the length of the tuple, this will not work:
const oops = [1, 2]; // number[]
requireTwoSameLengthArrays(oops, [1, 2, 3]); // okay because both are of unknown length
The type of oops
is inferred as number[]
, and passing it into requireTwoSameLengthArrays()
can't undo that inference. It's too late. If you want the compiler to just reject arrays of completely unknown length, you can do it:
function requireTwoSameLengthTuples<
T extends (readonly [] | readonly any[]) & (
number extends T["length"] ? readonly [] : unknown
)>(t: T, u: { [K in keyof T]: any }): void { }
This is uglier, but what it's doing is checking to see if T
has a length of number
instead of some specific numeric literal. If so, it prevents the match by demanding an empty tuple. This is a little weird, but it works:
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.
Okay, hope that helps; good luck!
Playground link to code
This is possible by requiring the consumer to pass it a length generic type to the function
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
)
The only limitations are that you need to pass in the <2>
or else typescript is kind enough to infer the generic number
for N
, also you need to declare all arguments as const
to have them lose the tuple length through type erasure
To get it working with in a react render function you'd need to do some additional some ugly TypeScript conditional types
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With