If I Have a Typescript Type of an Object, How Can I Use the Type

ghz 8months ago ⋅ 83 views

If I Have a Typescript Type of an Object, How Can I Use the Types of its Values?

I have a function for extracting nested GraphQL data:

const data = {
   cats: { edges: [ {node: cat1}, {node: cat2} ] },
   dogs: { edges: [ {node: dog1}, {node: dog2}, {node: dog3} ] },
}

const extracted = extract<{ cats: CatType, dogs: DogType }>(data);

// extracted = {
//   cats: [cat1, cat2], // CatType[]
//   dogs: [dog1, dog2, dog3], // DogType[]
// }

However, I'm having difficulty getting my type signature right. What I want to say is "This function takes an arg of an interface with string keys and type values, and it returns an object with those same keys, and values of arrays of their corresponding type".

In other words, if the function got { cats: nestedCats } it should return { cats: CatType[] } (ie. cats: T['cat'])). If it got { dogs: nestedDogs } it should return { dogs: DogType[] }, fish { fishes: FishType[] }, and so on. And if it gets multiple types, it should return an object with arrays of all of them.

Right now I have a signature of:

export const extractNodesFromData = <T extends { [key: string]: any }> =>

However, I have two problems:

  • A) inside my function, as I iterate through data, how can I refer to that data's type? For instance:

    Object.entries(data).map(([key, value]) => // How do I type value?

    Object.entries(data).map((([key, value: T[key]]) => // Doesn't work

  • B) I can't specify the function's return type: I can't figure out how to convert my provided type (of { cat: CatType, ...}) into the same thing, but for arrays (ie. { cat: CatType[], ...}) ... which is what the function will actually return.

Full Reproducible Example

This adds nothing to the actual question, but some might find it helpful to see the other (irrelevant) JS/TS:

interface CatType {
   id: string;
   name: string;
   catBreed: string;
}
interface DogType {
   id: string;
   name: string
   dogBreed: string;
}

const extractNodes = <T>(data): T[] =>
    data?.edges?.map(({ node }) => node);

const extractNodesFromData = <T extends { [key: string]: any }> (data: any) => 
Object.entries(data).reduce(
    (extracted, [key, value: T[key]]) => ({
      ...extracted,
      [key]: extractNodes<T[key]>(value)
    }),
    {}
  )

Answers

For your TypeScript function extractNodesFromData, you're on the right track with using generics to represent the type of the input data. Here's how you can refine your function signature and implementation to address your concerns:

type NodeMap = { [key: string]: any };

interface ExtractedData {
  [key: string]: any[];
}

function extractNodesFromData<T extends NodeMap>(data: T): ExtractedData {
  return Object.entries(data).reduce((extracted: ExtractedData, [key, value]) => {
    extracted[key] = extractNodes(value);
    return extracted;
  }, {});
}

function extractNodes<T>(data: { edges?: { node: T }[] }): T[] {
  return data?.edges?.map(({ node }) => node) || [];
}

// Test data
const data = {
  cats: { edges: [{ node: { id: '1', name: 'Cat1', catBreed: 'Breed1' } }, { node: { id: '2', name: 'Cat2', catBreed: 'Breed2' } }] },
  dogs: { edges: [{ node: { id: '3', name: 'Dog1', dogBreed: 'Breed1' } }, { node: { id: '4', name: 'Dog2', dogBreed: 'Breed2' } }, { node: { id: '5', name: 'Dog3', dogBreed: 'Breed3' } }] },
};

// Extracted data
const extracted = extractNodesFromData(data);

console.log(extracted);

Explanation:

  • We define a NodeMap type alias to represent the shape of the input data.
  • We define an ExtractedData interface to represent the shape of the extracted data, where each key corresponds to an array of extracted nodes.
  • The extractNodesFromData function takes input data of type T (where T extends NodeMap) and iterates over its entries. For each key-value pair, it calls extractNodes to extract the nodes from the value and stores them in the extracted data object.
  • The extractNodes function takes an object with an edges property (which contains an array of nodes) and extracts the nodes from it.

With this setup, TypeScript will infer the correct types for the value parameter inside the reduce function. Also, the return type of extractNodesFromData will be inferred correctly based on the input data structure.