15

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'>
2
  • 5
    If you're expecting one to one pairings of tabs to panels, then your data structure should really be something that supports that naturally (e.g. a tuple of [Tab, Panel] as Daniel suggested below or a new type that requires individual Tab and a Panel fields provided i.e. { readonly tab: Tab, readonly panel: Panel }). Commented Jun 5, 2020 at 1:43
  • That's a good point, and is what I ended up doing. Still very interested in some of the answers here though! Commented Jun 5, 2020 at 16:39

4 Answers 4

24

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

Sign up to request clarification or add additional context in comments.

6 Comments

Well now that is just nifty. Thank you!
Is the trick with using adding readonly [] documented anywhere? I'm wondering where else I could have found it if I didn't come across this answer.
In the latter case, I'd suggest never instead of readonly [] to represent the invalid case.
@rofer It's 4 years later but I'd like to point it out for future readers: it's not a special "trick". It's just the simple fact that [number, number] extends [] but doesn't extends number[]; therefore if you only write T extends number[], it prevents the compilers from inferring T as [number, number].
But [number, number] does not extend [] and it does extend number[]. You can verify that yourself. Was the prev comment a typo?
|
2

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

1 Comment

Any idea why you need to explicitly state the length of the array? Why is it not inferred from the arguments? stackoverflow.com/questions/62206735/…
1

Here's a partial answer for you:

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

Explanation

Because we are essentially using the length property of the array to check that they're equal, the array needs to be treated as immutable, hence the use of the readonly and as const keywords.

What we do is declare that we have an array, and that array always have length N.

Now, what I don't understand about this code, is why the generic argument in myFunction<3> is required. I don't know why the value of N isn't being inferred by arguments passed in.

Now, in your case, you might be better of just checking the array lengths at runtime and throwing an error. Remember that typescript is for developer experience, typically in catching in errors before they occur, but in this instance, doing a runtime error might be the quickest and easiest way to catch this kind of error.

1 Comment

I have asked a question about the inferrence problem here: stackoverflow.com/questions/62206735/…
1

Iterating on @jcalz answer and with inspiration from @cancerbero, I came upon a slightly different strategy which works well enough in my case:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.