This is my code, I expected bike({name:"bike"})
, but it’s throwing an error. I don’t know why it’s behaving this way.
const crateBox = <BikeData, CarData>()=>{
const bike = (data:BikeData)=>{
console.log("bike data: ", data);
}
const car = (data:CarData)=> {
console.log("car data: ", data);
}
return [bike, car];
}
const [bike, car] = crateBox<{name:string}, {type:string}>();
bike({name:"bike"})
The Playground:
But if I change const [bike, car] = createBox<{name:string}, {type:string}>();
to const {bike, car} = createBox<{name:string}, {type:string}>();
, it works correctly. However, I want to use the format [bike, car]
and not encounter an error.
How to solve this problem?
const crateBox = <BikeData, CarData>()=>{
const bike = (data:BikeData)=>{
console.log("bike data: ", data);
}
const car = (data:CarData)=> {
console.log("car data: ", data);
}
return {bike, car};
}
const {bike, car} = crateBox<{name:string}, {type:string}>();
bike({name:"bike"})
2
Answers
Add a constant assertion on the return statement.
By default, when a TypeScript array is inferred, the compiler assumes it can be mutable and allows modifications to its elements. However, in this specific case, you want to enforce that the returned array is a tuple with fixed types. The constant assertion ensures that the array’s inferred type is preserved as a tuple with the exact types of bike and car functions.
Because you didn’t annotate the return type of
crateBox()
, TypeScript infers the type from the value you returned, which was the array literal[bike, car]
. Type inference relies on heuristic rules to choose the most appropriate type. For array literals, the compiler assumes that the developer wants a plain array type, which represents an unordered list of arbitrary length. People often modify arrays by adding elements to them, removing elements from them, or reordering them. So this is a reasonable assumption.But that means
[bike, car]
is inferred as type(((data: BikeData) => void) | ((data: CarData) => void))[]
. So all the compiler keeps track of is that each element of that array is either aBikeData
consumer or aCarData
consumer. It doesn’t know the locations of them in the array or how long the array is.That doesn’t meet your needs. The assumption that you wanted an unordered arbitrary-length array type was incorrect.
What you apparently want is for the return type of
crateBox()
to be a tuple type. A tuple type is an array whose length and types of elements at each position are known and fixed. In your case, it would look like the type[(data: BikeData) => void, (data: CarData) => void]
, an array of length 2, whose first element is aBikeData
consumer, and whose send element is aCarData
consumer.Now, you could just annotate the return type and give up entirely on inference:
That works inside the function because now the compiler knows the exact type you wanted, and it can verify that
[bike, car]
matches that type. And it works outside the function because now the type ofbike
is the first element of the tuple type, and thus(data: { name: string }) => void
.But you don’t need to go that far. Instead you can use a
const
assertion on the[bike, car]
expression you’re returning. This still uses type inference, but it tells the compiler that you really want it to pay attention to the exact literal value; array literals should be given tuple types, and other primitive literals should be given literal types. If you do that:Now the return type of
crateBox
isreadonly [(data: BikeData) => void, (data: CarData) => void]
. This is areadonly
tuple type which isn’t known to have mutating methods likepush()
orsort()
, and won’t let you reassign the values. But otherwise it’s the same as the annotated tuple type[(data: BikeData) => void, (data: CarData) => void]
from the previous section.And that means the compiler will know that the first element of the returned array is a
BikeData
consumer, and the second is aCarData
consumer, and thus you get the behavior you want with minimal changes to your code:Playground link to code