Suppose I have function foo(args) {...}
where args
is an array of tuples such that the entries in the tuple are of the same type (i.e. [T,T]
), but entries across tuples may change 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 input the args
arguments of foo
so that mismatched types in the tuple throw a compile-time type error? For example:
foo([ [1, 3], ["hello", 5], // type error here [true, true], [2, 7n] // type error here ])
If the type error cannot be shown inline, it is also acceptable to make the entire function call wrong.
Appendix: Is it possible to use a 2-tuple of type [SomeType
(i.e. the second entry should be of the same type as the first), but T Can still vary between tuples [[SomeType
?
foo([ [{value: 1}, 3], [{value: "hello"}, 5], // type error here [{value: true}, true], [{value: 2}, 7n] // type error here ])
I think you can achieve this simply by creating a type for
row
that will accept astring
,number
, or boolean value.Type Row = string[] |Boolean value[]|Number[]
Now we can assign this type to the
args
arguments of thefoo
function.With this type definition, Typescript will throw an error if you provide an argument to
foo
where the types of the elements in the row do not match.This is the playground
Link代码>
.In order to achieve this, we need to use Generic array and mapping type to map the elements of the array. Since we know that the array should be an array of tuples of length 2, we will infer the generic parameter for the first item in the tuple and make the second item have the same type. To get the type of a generic parameter, we need to infer the keyword using the . Note that we need to know exactly (or at least a similarly shaped type) the generic type used to make it work, which in our case is
Variable
:It may seem like that’s all, but let’s look at the types of the following arrays:
As you can see, the type is not exactly the same as what we have in arr. The compiler extends the type to ensure that we can change array elements. To let the compiler know that the array is read-only, we need to use the const assertion:
Looks good now, this means we need to set the array passed to
foo
to read-only`, and since read-only arrays are a superset of the mutable arrays we would get if we tried Passing a read-only array to an array gives an error:Therefore, we update all array types in
foo
to be read-only. Note that since our array is two-dimensional, the inner array will also be read-only, and the array's constraints should be read-only arrays of read-only arrays:test:
However, we still have some problems. For example, if the first element in the tuple is
Variable
, it means that the second parameter should also be7
, not any number, if this is a The problem is that we need to get the primitive of7
, which is a number. This can be done using the ToPrimitive utility type from my type-samurai open source project: p>Update function:
Another problem is that if the inferred type in our current
foo
implementation isnumber[]
, we will not allow read-only arrays:The fix is very simple, we will check if the inferred type is some array, then get its element type and pass readonly ElementType[] as the second parameter in the tuple:
test:
The annoying part is that we need to use
const assertions
everywhere. In Typescript5.0
, const type parameters so we can avoidconst assertions
:Unfortunately we can't use them because we do something with the parameter instead of directly assigning
T
as a type:In short, currently,
const assertions
are the only way to ensure that it works as expected.Link to Playground