Konstantin Agapov
Konstantin Agapov

Reputation: 227

Type return value of function with generics in TypeScript

I was playing around with generics, and I bumped into code that I didn't quite understand, I have a function that receives a resp object; it could be of either type TSuccess or TFailure, they have different structures, and I want to get a return type according to the incoming type. For that, I have used generics. It looks like the code is running, but I still have squiggly lines and the error

"Type '"successful"' is not assignable to type 'T extends TFailure ? "failed" : T extends TSuccess ? "successful" : never'.(2322)"

I ran code on Stackblitz, click to follow to playground

Am I doing something wrong here?

THANK YOU FOR ANSWERING!👍✌️

type TSuccess = {
  success: {
    result: 'successful';
  };
};

type TFailure = {
  error: 'failed';
};

  const handleResp = <T extends TSuccess | TFailure>(
    resp: T
  ): T extends TFailure
    ? TFailure['error']
    : T extends TSuccess
    ? TSuccess['success']['result']
    : never => {
    if ('success' in resp) {
      if ('result' in resp.success) {
        return resp.success.result;
      }
    } else {
      return resp.error;
    }
    throw new Error('hahaa');
  };

  const r1 = handleResp<TSuccess>({ success: { result: 'successful' } });
  // r1 = 'successful' 
  const r2 = handleResp<TFailure>({ error: 'failed' });
  // r2 = 'failed'

Upvotes: 1

Views: 101

Answers (2)

geoffrey
geoffrey

Reputation: 2444

I could be tired, but I see nothing generic about this code.

In this very specific use case, since the path is different for each variant, your function needs to know how to get to the result for each variant, so it's not generic in that sense, and since the result for each variant is a static string, you could just as well use that string in overloads like in the following, nothing generic there either:

function handleResp(resp: TSuccess): "successful"
function handleResp(resp: TFailure): "failed"
function handleResp(resp: TSuccess | TFailure): "successful" | "failed"  {
  if ("success" in resp) {
    if ("result" in resp.success) {
      return resp.success.result;
    } else {
      throw new Error("missing result in response");
    }
  } else {
    return resp.error;
  }
};

I'm probably missing something, and I don't mean that the previous is what you should use, I'm just confused.

If that use case is real, I would prefer my attempt over Tobias' simply because there is no generic or conditional involved, it's less elaborate; if your use case is actually different, maybe you could tell us more about it.

Upvotes: 0

Tobias S.
Tobias S.

Reputation: 23825

The TypeScript compiler does not understand how a conditional type with a generic corresponds to your return statements in the function implementation. It just sees that the conditional starts with T extends ... and stops reasoning with it.

You could use type assertions to tell the compiler that you know the type is correct. I would go for a function overload instead. We can separate the generic types from the actual function implementation.

function handleResp<T extends TSuccess | TFailure>(resp: T): T extends TFailure
  ? TFailure["error"]
  : T extends TSuccess
  ? TSuccess["success"]["result"]
  : never
function handleResp(
  resp: TSuccess | TFailure
):  TFailure["error"] | TSuccess["success"]["result"]  {
  if ("success" in resp) {
    if ("result" in resp.success) {
      return resp.success.result;
    }
  } else {
    return resp.error;
  }
  throw new Error("hahaa");
};

Playground

Upvotes: 1

Related Questions