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 typeT
(whereT
extendsNodeMap
) and iterates over its entries. For each key-value pair, it callsextractNodes
to extract the nodes from the value and stores them in the extracted data object. - The
extractNodes
function takes an object with anedges
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.