Pesquisa de site

typescript - Tipagens dinâmicas baseadas no valor em objeto


Temos uma resposta de API para a qual estamos procurando melhorar nossas tipagens, vou simplificar para este exemplo. Digamos que a API retorne algo assim, onde há um tipo de valor e um valor associado.

Nota: Infelizmente, esta é uma API existente e estamos apenas tentando trabalhar com o que ela oferece na forma atual.

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

Com esta resposta, na verdade há uma relação direta entre os tipos valueType e value. Então, para tentar melhorar o suporte de tipagem para a resposta da API, eu tentei algo assim.

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 },
  },
];

Isso funciona muito bem até agora, onde as tipagens garantem a forma correta dos dados quando tipados estaticamente como estão acima. Se você tentar mudar o valueType de cat para bird no primeiro objeto, isso resultaria em um erro de tipo.

Agora o problema surge quando eu tento interagir com esses dados de uma maneira dinâmica e tento usar um guardião de tipo.

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; }

Então, nesse caso, embora a digitação inicialmente pareça funcionar, ela falha ao operar como esperado com guardas de tipo, o que ainda significa que precisamos fazer casting.

Então, existe uma maneira de alcançar essa funcionalidade através de outro método de digitação?

Resposta #1

O Typescript parece ter dificuldade em restringir o objeto pai a partir do objeto interno. Gostaria que houvesse uma forma elegante de lidar com isso, mas como @kelly aponta na resposta, provavelmente você vai precisar de protetores de tipo aqui. (Esse fato me surpreende, mas parece ser verdade.)

Mas para evitar que você tenha uma função diferente para cada tipo possível, você pode usar uma função genérica para fazer a verificação por você:

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

Aqui, pegamos o tipo a ser verificado como T. E usamos Extract para filtrar a união para apenas as correspondências.

Então o uso parece, e parece visualmente, muito bom.

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

Veja o parquinho

Resposta #2

Usando predicados de tipo, é possível restringir o tipo você mesmo:

function isCat(data: APIResponseType): data is Extract<APIResponseType, { meta: { valueType: "cat" } }> {
    return data.meta.valueType === "cat";
}

sampleResponse.forEach((animal) => {
  if (isCat(animal)) {
    console.log(animal.value.paws);
  }
});

Parque infantil

O TS não permite que você use paws porque não há garantia de que animal não mude entre o momento em que você verifica e o momento em que você o utiliza (o TS não faz esse tipo de análise estática).

Artigos relacionados