Search code examples
typescripttypescript-genericsmsgpack

Narrow unknown type to generic type parameter


I have a function to call a web socket that returns a serialized result, which is then deserialized. The resulting type of the deserialization (with msgpack) is unknown.

Thus I want to provide a generic type parameter for the calling function, so that the type of the result can be passed into the call function.

My attempt is this:

function call<T>(response: unknown): T {
  return response; // Type 'unknown' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'unknown'. (2322)
}

TS Playground

I could restrict the generic type, but this is the simplest reproduction that shows my problem.

Is there a way to achieve this with a generic type parameter or is there a better way of providing the possibility to pass in the expected type?


Solution

  • As one of the comments noted, the only way to make your example work with precisely that signature is with an assertion. So, just change your return to return response as T.

    But I think there's a better way to handle this type of situation, i.e., figuring out the return type of a deserialized response.

    I never really like solutions that involve using a type parameter to tell a function what type to return when the type would otherwise be unknown because it comes from some outside source, e.g., an API call. You are basically telling the compiler I know I will definitely get back this type. I guess there are situations where that could be true, but in an API call I would think the more likely scenario is that you know what you will probably get back, but you aren't 100% sure. After all, the API could change, the server could have a bug, etc.

    I think the better way is to use a user defined type guard to actually check what the type is what you think it is. That gives you the correct type AND real type safety.

    The simplest way to is keep the return type as unknown and then pass it to a type guard outside your function. So, for example, assuming you expect a return type of Foo:

    function isFoo(response: unknown): response is Foo {
      // code that checks the form of the response and
      // returns true if valid
    }
    
    function call(response: unknown):unknown {
      return response; 
    }
    
    const myResponse
    const myReturnedResponse = call(myResponse)
    if(isFoo(myReturnedResponse)) {
      //normal execution
    } else {
      // error handling
    }
    

    But you can do even better than that. You can keep your call function but instead of providing the information about your expected type via a type parameter, actually pass in the typeguard itself as a runtime parameter. That will let you keep the basic signature you have above that starts with an unknown parameter and returns a typed response, but now you have real type safety.

    So--

    function call<T>(response:unknown, isCorrectType: (x:unknown)=>x is T):T {
      if(isCorrectType(response)) {
         return response
      } else {
         throw("Invalid response")
      }
    }
    

    Note there is still a generic type parameter here, but rather than you needing to put it in explicitly (which is really just an assertion), it is being inferred from the return type of the type guard you pass in.

    Here's a working example in a playground.