typescriptgenericstypestype-safetynested-generics

Is it possible to return generic type from argument passed to a function


I've thoroughly commented code below. I feel like it's much easier to understand the problem like this instead of trying to explain it just with the worlds.

abstract class BaseEvent<REQUEST_PAYLOAD, RESPONSE_PAYLOAD> {
  constructor(public requestPayload: REQUEST_PAYLOAD) {}

  // this method was only created for the sake of showing response1 type example
  public return_RESPONSE_PAYLOAD_type(): RESPONSE_PAYLOAD {
    return null;
  }
}

type GetUserInfoRequest = { userId: number };
type GetUserInfoResponse = { username: string; age: number };

class GetUserInfoEvent extends BaseEvent<
  GetUserInfoRequest,
  GetUserInfoResponse
> {}

const emit = async <
  REQUEST_PAYLOAD,
  RESPONSE_PAYLOAD,
  EVENT extends BaseEvent<REQUEST_PAYLOAD, RESPONSE_PAYLOAD>
>(
  event: EVENT
): Promise<RESPONSE_PAYLOAD> => {
  // some stuff will be done there - for the sake of example it was removed
  return null;

  // return event.return_RESPONSE_PAYLOAD_type(); // doesn't work aswell
};

const main = async () => {
  const event = new GetUserInfoEvent({ userId: 666 });
  const response1 = event.return_RESPONSE_PAYLOAD_type(); // type === { username: string; age: number; }
  const response2 = await emit(event); // type === {} but I want it to be { username: string; age: number; }

  // response2.username <-- error
  // response2.age <-- error

  // I want emit function to return current RESPONSE_PAYLOAD type instead of an empty object. I don't want to manually cast returned types to GetUserInfoResponse because I want to achieve 100% type safety.
};

main();

I should probably not that it was tested with typescript@3.2.4.


Solution

  • Use can use a conditional type to extract the type argument from EVENT. Type parameters are not inferred one based on another usually, and you end up with the narrowest possible type (in this case {})

    abstract class BaseEvent<REQUEST_PAYLOAD, RESPONSE_PAYLOAD> {
        constructor(public requestPayload: REQUEST_PAYLOAD) { }
    
        // this method was only created for the sake of showing response1 type example
        public return_RESPONSE_PAYLOAD_type(): RESPONSE_PAYLOAD {
            return null;
        }
    }
    
    type GetUserInfoRequest = { userId: number };
    type GetUserInfoResponse = { username: string; age: number };
    
    class GetUserInfoEvent extends BaseEvent<
        GetUserInfoRequest,
        GetUserInfoResponse
        > { }
    // Conditional type to extract the response payload type:
    type RESPONSE_PAYLOAD<T extends BaseEvent<any, any>> = T extends BaseEvent<any, infer U> ? U : never; 
    const emit = async <
        EVENT extends BaseEvent<any, any>
    >(
        event: EVENT
    ): Promise<RESPONSE_PAYLOAD<EVENT>> => {
        // some stuff will be done there - for the sake of example it was removed
        return null;
    
        // return event.return_RESPONSE_PAYLOAD_type(); // doesn't work aswell
    };
    
    const main = async () => {
        const event = new GetUserInfoEvent({ userId: 666 });
        const response1 = event.return_RESPONSE_PAYLOAD_type(); // type === { username: string; age: number; }
        const response2 = await emit(event); // is now GetUserInfoResponse
    
        response2.username //<-- ok
        response2.age //<-- ok
    
    };