简体   繁体   中英

Types of property 'type' are incompatible. Problem with Typescript union types

Background

I am building a react site with some reusable generic UI components. Our backend service will return some responses with the data conforming to an abstract type.

For example

interface TypeAServerResponse {
  somefield: string,
  otherfield: string,
}

interface TypeBServerResponse {
  somefield: string,
}

type TypeServerResponseUnion = TypeAServerResponse | TypeBServerResponse;

Both of the server response types contain somefield , and we would like to display that in the reused UI component. So we union them and tell the component to expect TypeServerResponseUnion .

However, in some occasions, we would also want to use otherfield , so we need to tell TypeScript to we are discriminating the union type. Without changing the backend to return a string literal, we are extending the ServerResponse types to contain a type string literal.

interface TypeA extends TypeAServerResponse{
  $type: 'a',
}

interface TypeB extends TypeBServerResponse{
  $type: 'b',
}

type TypeUnion = TypeA | TypeB; //or
type TypeUnion = TypeServerResponseUnion extends {
    $type: 'a'|'b',
}

Now we can check on $type field in our UI component to discriminate the union and get otherfield when possible.


The problem

We now have some method to fetch the data from the server that returns TypeServerResponseUnion , and we want to parse it to TypeUnion before providing it to the UI layer.

// could be ajax.get, could be axios
const serverGet = () : TypeServerResponseUnion => {
  return {somefield: 'something'}
}

const parse = () : TypeUnion => {
  const response : TypeServerResponseUnion = serverGet();

  // do something here to add the $item field and return it

}

We have two use cases

  1. We know which concrete type we are asking for, so we can just provide the $type to the function. This has some problems I don't know how to deal with.
  2. We don't know which concrete type we are asking for, we only know we are asking for a same type as we already have, this is the part where I am struggling with.

So I have the parse function as such:

const get = (original: TypeUnion) => {
  const response = serverGet();

  const parsedResponse: TypeUnion = {...response, $type: original.$type}
  return parsedResponse
}

It complains with error:

Type '{ $type: "a" | "b"; somefield: string; otherfield: string; } | { $type: "a" | "b"; somefield: string; }' is not assignable to type 'TypeUnion'.
  Type '{ $type: "a" | "b"; somefield: string; }' is not assignable to type 'TypeUnion'.
    Type '{ $type: "a" | "b"; somefield: string; }' is not assignable to type 'TypeB'.
      Types of property '$type' are incompatible.
        Type '"a" | "b"' is not assignable to type '"b"'.
          Type '"a"' is not assignable to type '"b"'.(2322)

So I want to know what is the best way to do typing for those types to solve this use case we are facing.


Extension

I also want to discuss this related problem with typescript. If I change the get function to the following:

const get = (original: TypeUnion) => {
  const response = serverGet();
  const parsedResponse: TypeUnion = {} as TypeUnion;
  parsedResponse.$type = original.$type
  return parsedResponse

The error goes away, but of course because we are doing wrong type casting so it is not safe.

The question is why can we assign original.$type to TypeUnion.$type , where previously

const parsedResponse: TypeUnion = {...response, $type: original.$type}

we are assigning original.$type to $type during construction time does not work.

Playground with code

The reason for the type error is because you do not know if original and response are of the same type. Since they are both unions one could be of type "A" and the other of type "B". This might be be the true in the real implementation but it's not true in terms of the types.

We know which concrete type we are asking for, so we can just provide the $type to the function. This has some problems I don't know how to deal with.

This sorta seems like an anti pattern to me. If you know what type is coming from the backend then serverGet should not return a union but have the return type TypeAServerResponse or TypeBServerResponse

We don't know which concrete type we are asking for, we only know we are asking for a same type as we already have, this is the part where I am struggling with.

For this it is probably best to write a helper parse function (as you already did) but have but parse based on the data and not a value that is passed in.

const getPraseValueFromSErver: ()=>TypeUnion= () => {
  const res = serverGet();

  if("otherfield" in res){
    // we know we have type A since otherfield exsists in the data
    return { $itemType: "a", ...res}
  } else {
    // we know we have type B
    return { $itemType: "b", ...res}
  }
}

See full playground here

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM