Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic typings based on value in object

Tags:

typescript

We have an API response we are looking to improve our typings for, I'll simplify for this example. Say the API returns something like this where there is a value type and an associated value.

Note: Unfortunately this is an existing API and we are just trying to work with what it provides in the current shape.

{
  meta: {
    id: string;
    name: string;
    valueType: 'cat' | 'bird'
  }
  value: { paws: number } | { wings: number }
}

With this response there is actually a direct relationship between the typings valueType and value. So to try and improve the typing support for the API response I've tried something like this.

type MetaType = {
  id: string;
  name: string;
};

type APIResponseType =
  | {
      meta: MetaType & {
        valueType: 'cat';
      };
      value: { paws: number };
    }
  | {
      meta: MetaType & {
        valueType: 'bird';
      };
      value: { wings: number };
    };

const sampleResponse: APIResponseType[] = [
  {
    meta: {
      id: '123',
      name: 'Garfield',
      valueType: 'cat',
    },
    value: { paws: 4 },
  },
  {
    meta: {
      id: '123',
      name: 'Woody',
      valueType: 'bird',
    },
    value: { wings: 2 },
  },
];

This works great so far, where the typings ensure the proper shape of the data when statically typed like they are above. If you tried changing the valueType from cat to bird in the first object, it would result in a type error.

Now the problem comes up when I try to interact with this data in a dynamic way and try to use a type guard.

sampleResponse.forEach((animal) => {
  if (animal.meta.valueType === 'cat') {
    console.log(animal.value.paws);
  }
});

// TypeScript see's the type for value as this:
(property) value: { paws: number; } | { wings: number; }

So in this case although the typing initially seems to work, it fails to function as expected with type guards which still means we need to do casting.

So is there a way to achieve this functionality through another method of typing?

like image 585
Trevor Wright Avatar asked Nov 06 '25 21:11

Trevor Wright


1 Answers

Typescript seems to have a hard time narrowing the parent object, from the inner object. I wish there was an elegant way to handle that, but as @kelly points out in their answer, you will probably need type guards here. (This fact is a surprise to me, but it appears to be true.)

But to prevent you having a different function for every possible type, you can use a generic function to do the check for you:

function checkResponseType<
  T extends APIResponseType['meta']['valueType']
>(
  res: APIResponseType,
  type: T
): res is Extract<APIResponseType, { meta: { valueType: T }}> {
  return res.meta.valueType === type
}

Here we take the type to check as T. And we use Extract to filter down the union to only the matches.

Then usage feels, and looks, pretty nice.

sampleResponse.forEach((animal) => {
  if (checkResponseType(animal, 'cat')) {
    console.log(animal.value.paws); // fine
  }
});

See playground

like image 179
Alex Wayne Avatar answered Nov 09 '25 16:11

Alex Wayne



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!