skip to Main Content

I try to implement a function which will return different types following its parameters.

Something like the function below, but more efficient (with generic and without the union type as result)

type ResultType = {
  get: GetResult
  post: PostResult
  ...
}

function fetch(operation: keyof ResultType): GetResult | PostResult | ...

So I start with the example provided in: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html.
When I try to implement the function of the example :

interface IdLabel {
  id: number /* + d'autres champs */
}
interface NameLabel {
  name: string /* + d'autres champs */
}

type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  if (typeof idOrName === 'number') {
    return { id: idOrName }
  } else {
    return { name: idOrName }
  }
}

I’ve got the following errors that I cannot understand:

Type '{ id: number; }' is not assignable to type 'NameOrId<T>'.
Type '{ name: string; }' is not assignable to type 'NameOrId<T>'.

What did I do wrong?

Bonus question: Is there another way in TS to manage lot of conditional cases (kind of switch) ?

2

Answers


  1. Well, you’re right: discriminated unions don’t help much.

    I think I’ve found a solution, but I don’t know if you like that. Feel free to consider or reject.

    The idea is based on Indexed Access Types.

    Let’s imagine a series of types, as you suggested, even a little complex. Note that I like more type than interface, but I think should work the same.

    type TIdLabel = {
      id: number /* + d'autres champs */
    }
    
    type TNameLabel = {
      name: string /* + d'autres champs */
    }
    
    type TMixedLabel = {
      num: number
      text: string;
      flag: boolean;
    }
    

    At this point, just define our function, along its input and output types. I assigned each type to a specific key, as index.

    type TResult = {
      "id": TIdLabel,
      "name": TNameLabel,
      "mixed": TMixedLabel,
    }
    
    type TProps = {
      "id": number,
      "name": string,
      "mixed": {
        a: number;
        b: number;
      },
    }
    
    function createLabel<T extends keyof TResult>(type: T, props: TProps[T]): TResult[T] {
      if (type === "id" && typeof props === "number") {
        return { id: props } as TResult[T];
      }
      else if (type === "name" && typeof props === "string") {
        return { name: props } as TResult[T];
      }
      else if (type === "mixed" && typeof props === "object" && "a" in props && "b" in props) {
        const { a, b } = props;
        return { num: a + b, text: "Hello!", flag: true } as TResult[T];
      }
      else{
        throw new Error();
      }
    }
    

    So far, the only "ugly" part is the discrimination of the input. That has to be at runtime and the type argument comes to rescue.

    The usage is pretty straightforward, although you’ve to specify the type as discriminator.

    const x1 = createLabel("id", 123);        //x1 is TIdLabel
    const y1 = createLabel("name", "Betty");  //y1 is TNameLabel
    const z1 = createLabel("mixed", { a: 6, b: 9 });  //z1 is TMixedLabel
    //const w1 = createLabel("fail");  <-- won't compile!
    

    It seems that the trick can be done even without the explicit discrimator. However, it depends on the ability to discriminate the cases against the input.

    function createLabel2<T extends keyof TResult>(props: TProps[T]): TResult[T] {
      if (typeof props === "number") {
        return { id: props } as TResult[T];
      }
      else if (typeof props === "string") {
        return { name: props } as TResult[T];
      }
      else if (typeof props === "object" && "a" in props && "b" in props) {
        const { a, b } = props;
        return { num: a + b, text: "Hello!", flag: true } as TResult[T];
      }
      else{
        throw new Error();
      }
    }
    
    const x2 = createLabel2(123);
    const y2 = createLabel2("Betty");
    const z2 = createLabel2({ a: 6, b: 9 });
    

    Here is the playground.

    Login or Signup to reply.
  2. This is a solution to the original problem you stated, that of relating the return type to the input type.

    You can use an indexed access type to specify the return type in terms of the input type.

    Unfortunately, TypeScript does not fully support this yet. There is an open issue at ms/TS#33014 for proper support under the name of "Dependent-Type-Like Functions". A workaround for your simple case is to assert the type of the returned values.

    The following code demonstrates a possible approach. I have had to give my own definitions of the missing types.

    type GetResult = {
      getData: string
    }
    
    type PostResult = {
      postData: string
    }
    
    type ResultType = {
      get: GetResult
      post: PostResult
    }
    
    function fn<T extends keyof ResultType>(operation: T): ResultType[T] {
      if (operation === "get") {
        return { getData: "foo" } as ResultType[T]
      } else {
        return { postData: "bar" } as ResultType[T]
      }
    }
    
    const res1 = fn("get")
    //     ^? GetResult
    const res2 = fn("post")
    //     ^? PostResult
    const res3 = fn("put") // Error: "put" is not a key of ResultType
    

    Playground of the above

    If you have many operations, you could use a function map. Defining ResultType in terms of that would it all less error prone, as you wouldn’t have to make sure types match despite assertions; instead, the types are basically asserted to be themselves.

    Here’s a possible implementation of that:

    type GetResult = {
      getData: string
    }
    type PostResult = {
      postData: string
    }
    
    const operations = {
      get(): GetResult {
        return { getData: "foo" }
      },
      post(): PostResult {
        return { postData: "bar" }
      }
    }
    
    type ResultType = {
      [key in keyof typeof operations]: ReturnType<typeof operations[key]>
    }
    
    function fn<T extends keyof ResultType>(operation: T): ResultType[T] {
      return operations[operation]() as ResultType[T]
    }
    

    Playground of above

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