reactjstypescriptgraphqlreact-querygraphql-codegen

typescript can't resolve typing from graphql query that returns a union type


I would like to understand what is going on here, and since i'm not very experienced with typescript, any help would be greatly appreciated !

I've got a resolver to return one cost from database :

  @Query(() => FixedOrVariableCost, { name: 'oneCost' })
  async getOneCost(
    @Args('id', { type: () => String }) id: MongooseSchema.Types.ObjectId
  ) {
    return this.costsService.getOneCost(id);
  }

Its return type FixedOrVariableCost is a union type :

const FixedOrVariableCost = createUnionType({
  name: 'FixedOrVariableCost',
  types: () => [FixedCost, VariableCost] as const,
});

Basically in my model, a cost is a fixed or a variable one. Cost is an abstract interface in my graphql layer.

Generated schema :

union FixedOrVariableCost = FixedCost | VariableCost

oneCost(id: String!): FixedOrVariableCost!

Here is the query i'm using :

const useGetOneCost = (id: string) =>
  useQuery(['getOneCost', id], async () => {
    return (
      graphQLClient.request(
        gql`
          query GetOneCost($id: String!) {
            oneCost(id: $id) {
              ... on Cost {
                _id
                kind
                code
                label
                title
                content
                costType {
                  _id
                  code
                  label
                }
                costMargin {
                  _id
                  code
                  label
                }
                createdAt
                createdBy {
                  _id
                }
                updatedAt
                updatedBy {
                  _id
                }
                status {
                  _id
                  code
                  label
                  color
                }
              }
              ... on FixedCost {
                fixedCostValue {
                  amount
                  currency {
                    _id
                    code
                    label
                    symbol
                  }
                }
              }
              ... on VariableCost {
                variableCostValue
              }
            }
          }
        `,
        { id }
      ),
      {
        enabled: !!id,
      }
    );
  });

The typing generated by graphql codegen is the following :

export type GetOneCostQuery = {
  __typename?: 'Query';
  oneCost:
    | {
        __typename?: 'FixedCost';
        _id: string;
        kind: CostKind;
        code: string;
        label: string;
        title?: string | null;
        content?: string | null;
        createdAt: any;
        updatedAt: any;
        costType: {
          __typename?: 'CostType';
          _id: string;
          code: string;
          label: string;
        };
        costMargin: {
          __typename?: 'CostMargin';
          _id: string;
          code: string;
          label: string;
        };
        createdBy: { __typename?: 'User'; _id: string };
        updatedBy: { __typename?: 'User'; _id: string };
        status: {
          __typename?: 'GenericStatus';
          _id: string;
          code: string;
          label: string;
          color: string;
        };
        fixedCostValue: {
          __typename?: 'FixedCostValue';
          amount: number;
          currency: {
            __typename?: 'Currency';
            _id: string;
            code: string;
            label: string;
            symbol: string;
          };
        };
      }
    | {
        __typename?: 'VariableCost';
        _id: string;
        kind: CostKind;
        code: string;
        label: string;
        title?: string | null;
        content?: string | null;
        createdAt: any;
        updatedAt: any;
        variableCostValue: number;
        costType: {
          __typename?: 'CostType';
          _id: string;
          code: string;
          label: string;
        };
        costMargin: {
          __typename?: 'CostMargin';
          _id: string;
          code: string;
          label: string;
        };
        createdBy: { __typename?: 'User'; _id: string };
        updatedBy: { __typename?: 'User'; _id: string };
        status: {
          __typename?: 'GenericStatus';
          _id: string;
          code: string;
          label: string;
          color: string;
        };
      };
};

very nice ! Typing generation works as expected

Now, one of my component receives a cost as a prop, so i'm picking oneCost inside GetOneCostQuery type

type PropType<TObj, TProp extends keyof TObj> = TObj[TProp];

type OneCost = PropType<GetOneCostQuery, 'oneCost'>;

type Props = {
  cost: OneCost;
};

export const MyComponent = ({
  cost
}: Props) => {
  console.log(cost.fixedCostValue);
  return null;
}

ts error on ``fixedCostValue :

La propriété 'fixedCostValue' n'existe pas sur le type '{ __typename?: "FixedCost" | undefined; _id: string; kind: CostKind; code: string; label: string; title?: string | null | undefined; content?: string | null | undefined; createdAt: any; ... 6 more ...; fixedCostValue: { ...; }; } | { ...; }'.
  La propriété 'fixedCostValue' n'existe pas sur le type '{ __typename?: "VariableCost" | undefined; _id: string; kind: CostKind; code: string; label: string; title?: string | null | undefined; content?: string | null | undefined; createdAt: any; ... 6 more ...; status: { ...; }; }'.ts(2339)

roughly in english : "Property 'fixedCostValue' doesn't exist on type blablabla"

Shouldn't this value maybe available, if cost is a fixed one ? Why can't typescript recognize this value ? This confuses me ...

Thanks for your help


Solution

  • Type OneCost is a union, meaning it is one of two objects - either FixedCost or VariableCost. While the first contains an attribute named fixedCostValue, the second one doesn't, hence the error.

    If you'll check if this attribute exists in the object, it'll pass. for example:

    export const MyComponent = ({
      cost
    }: Props) => {
      if ("fixedCostValue" in cost) {
        console.log(cost.fixedCostValue);
      };
      return null;
    }