Home > Enterprise >  Typescript: conditional generic Type
Typescript: conditional generic Type

Time:06-21

Is it possible to write a function with types (similar to the one at bottom) so that it can be used in the following ways. Either;

parseApiResponse(apiResponse); // returns TParsedResponse
// or
parseApiResponse<TSpecificParsedResponse>(apiResponse); // returns TSpecificParsedResponse

where

type TApiResponse = {/* some complex non-specific raw data */}
type TParsedResponse = {[key:string]: string | number}[] // array of objects with any string as a key
type TSpecificParsedResponse = {name:string, age:number}[]

Here's my shoddy first attempt

const parseApiResponse = <T extends {}>(apiResponse: TApiResponse): T => {
  //some code here
  return parsedResponse;
};

CodePudding user response:

This is a really good use case for extends in Typescript.

Here is the full code example

The main idea is that have a generic type that extends the base type.

function parseApiResponse<T extends TParsedResponse>(apiResponse: TApiResponse){
    // ... process
    const parsedResponce: T = [{}] as T
    return parsedResponce
}

Typescript will infer the "extended" type if it is not passed and will use the more specify one if it is passed.

Here is the full code example in case the Playground link above does not work.

type TParsedResponse = {[key:string]: string | number}[] // array of objects with any string as a key
type TSpecificParsedResponse = {name:string, age:number}[]

function parseApiResponse<T extends TParsedResponse>(apiResponse: TApiResponse){
    // ... process
    const parsedResponce: T = [{}] as T
    return parsedResponce
}

const exampleOne = parseApiResponse({complex: 'foo'}); // returns TParsedResponse
// or
const exampleTwo = parseApiResponse<TSpecificParsedResponse>({complex: 'foo'}); // returns TSpecificParsedResponse

CodePudding user response:

Defining the function is the easy part:

function parseApiResponse<T extends TParsedResponse>(apiResponse: TApiResponse): T {
  /* ... */
  return someResult as any;
}

The function above accepts a generic parameter that must extend a union of your two types and defaults to TParsedResponse when no type information is given.

Since the type of T is not available at runtime, the implementation of this method would difficult - unless you cast the return result as any. If you're okay trusting that the caller always knows the correct output type then you can stop here.

If it's not okay, then there are multiple ways you can handle this. Essentially you'll need to pass something that acts as a type guard so the compiler knows you're returning the right result.

One thing you can does is use a literal string that acts as a type identifier, and overload the function instead of making it generic:

function parseApiResponse(apiResponse: TApiResponse): TParsedResponse
function parseApiResponse(apiResponse: TApiResponse, type: 'specific'): TSpecificParsedResponse
function parseApiResponse(apiResponse: TApiResponse, type?: undefined | 'specific'): TParsedResponse {
    switch(type) {
        // instead of casts these should be some specific implementation.
        case 'specific': return apiResponse as TSpecificParsedResponse; 
        default: return apiResponse as TParsedResponse;
    }
}

You could avoid all of the overloading if there was a way to have a discriminating property in TApiResponse. You can read more about Discriminating Unions.

  • Related