skip to Main Content

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:

https://www.typescriptlang.org/play?#code/MYewdgzgLgBMBOBDKBTAQiAHjAvDAPGgJYDWKAIsogDQwDCi8lUiAfABQCUOrA3gFD8YwuOGgwARqRS4Y7ACZUAXMTLNE3PkJE7QkEABsUAOgMgA5uwBEUsjEUslMK7QcaA3NuEBfQbrGwwIyyCsoMTFSaMAI6-vpGphbWQfD2ys6ukZ46vl4w8ChQAK7wYDAA2rYotCkAup65-HrildI1jLWyCMjoWPi8YIgAtihK0PBEYObetLxQAJ4ADqPjk9McnJ78VewDw6M20lbenEA

enter image description here

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"})

https://www.typescriptlang.org/play?#code/MYewdgzgLgBMBOBDKBTAQiAHjAvDAPGgJYDWKAIsogDQwDCi8lUiAfABQCUOrA3gFD8YwuOGgwARqRS4Y7ACZUAXMTLNE3PkJE7QkEABsUAOgMgA5uwBEUsjEUslMK7QcaA3NuEBfQbrGwwIyyCsoMTFSaMAI6-vpGphbWQfD2ys6ukZ46vl4w8ChQAK7wYNG2KLQp3p65-HrivBVVjN6yCMjoWPi8YIgAtihK0PBEYObetLxQAJ4ADkMjYxMcnJ78Fey9A0M20lbenEA

2

Answers


  1. 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.

    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] as const; // here
    }
    
    
    const [bike, car] = crateBox<{name:string}, {type:string}>();
    
    bike({name:"bike"})
    
    Login or Signup to reply.
  2. 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 a BikeData consumer or a CarData 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 a BikeData consumer, and whose send element is a CarData consumer.

    Now, you could just annotate the return type and give up entirely on inference:

    const crateBox =
        <BikeData, CarData>(): [(data: BikeData) => void, (data: CarData) => void] => {
            // ------------> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            const bike = (data: BikeData) => { console.log("bike data: ", data); }
            const car = (data: CarData) => { console.log("car data: ", data); }
            return [bike, car]; // okay
        }
    
    const [bike, car] = crateBox<{ name: string }, { type: string }>();   
    bike({ name: "bike" }); // okay
    

    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 of bike 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:

    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] as const;
    }
    

    Now the return type of crateBox is readonly [(data: BikeData) => void, (data: CarData) => void]. This is a readonly tuple type which isn’t known to have mutating methods like push() or sort(), 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 a CarData consumer, and thus you get the behavior you want with minimal changes to your code:

    const [bike, car] = crateBox<{ name: string }, { type: string }>();
    bike({ name: "bike" }); // okay
    

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search