Say I have function foo(args) {...}
where args
is an array of 2-tuples such that the entries within the tuple are the same type (i.e. [T,T]
), but the entries across tuples may vary arbitrarily (i.e. [[T,T],[U,U],[V,V]]
). For example:
foo([
[1, 3],
["hello", "world"],
[true, true],
[2, 7]
]) // no error
How should I type the args
parameter of foo
so that mismatching types within a tuples raises a compile-time type error? For example:
foo([
[1, 3],
["hello", 5], // type error here
[true, true],
[2, 7n] // type error here
])
If it’s not possible to show the type error inline, making the whole function call error is also acceptable.
Addendum: Can this be made to work with 2-tuples of type [SomeType<T>, T]
(i.e the second entry’s type should match the generic of the first), but T can still vary between tuples [[SomeType<T>, T],[SomeType<U>, U],[SomeType<V>, V]]
?
foo([
[{value: 1}, 3],
[{value: "hello"}, 5], // type error here
[{value: true}, true],
[{value: 2}, 7n] // type error here
])
2
Answers
I think you can simply achieve this by creating a type for a
row
which will accept the array of eitherstring
,number
orboolean
.type Row = string[] | boolean[] | number[]
And now, we can just assign this type for
args
parameter forfoo
function.With this type definition, if you will provide an argument to
foo
where the types of elements with in a row did not match, Typescript will raise an error.Here is the playground
link
.To achieve this we will need to use generics for the array and mapped types to map through the elements of the array. Since we know that the array should be an array of tuples of length two, we are going to infer the generic parameter of the first item in the tuple and make the second one of the same type. To get the type of the generic parameter, we need to use the infer keyword. Note that we need to know exactly (or at least the one that has a similar shape) which generic type is used to make it work, which is
Variable
in our case:It may look like it is all, however let’s see the type of the following array:
As you can see, the type is not exactly what we have in the arr. The compiler widens the type to make sure that we can mutate the array elements. To let the compiler know that the array is read-only we will need to use const assertion:
Looks good, now, this means that we will need to make the array that we pass to the
foo
read-only` and since read-only arrays are the supersets of mutable arrays we will get an error if we try to pass a read-only array to just array:Thus, let’s update all array types in the
foo
to read-only. Note that since our array is two-dimensional, the inner arrays will be also read-only and the constraint for the array should be a read-only array of read-only arrays:Testing:
However, we still have some problems. For example, if the first element in the tuple is
Variable<7>
it will mean that the second argument should be also7
, not any number, and if that’s an issue we need to get the primitve of the7
which is number. This can be achieved using ToPrimitive utility type from my type-samurai open-source project:Updated function:
Another issue is if the inferred type is
number[]
in our currentfoo
implementation we won’t let the read-only arrays:The fix is pretty straightforward, we will check whether the inferred type is some array then we will get its elements type and write
readonly ElemenType[]
as the second argument in the tuples:Testing:
The annoying part is that we need to use
const assertion
everywhere. In the Typescript5.0
the const type parameters were added, which let’s avoidconst assertions
:Unfortunately, we are not able to use them, since we do some manipulation with the argument instead of directly assigning
T
as a type to it:In conclusion, for now, the
const assertion
is the only way to make sure that it works as expected.Link to playground