How can I get the type definition from a nested object?
In this example I get a list of entities(products) how can I access the type definition for the entity(node).
This is compiled using relay-compiler
export type ProductAutoSuggestQueryResponse = {
node: {
products?: {
edges: ReadonlyArray<{
node: {
name: string;
id: string;
currencies: ReadonlyArray<string> | null;
};
} | null> | null;
} | undefined;
} | null;
};
I have tried using Pick<>
but it seem to get very complex and the array is causing me issue.
Conceptually you want to take ProductAutoSuggestQueryResponse
and index into it deeply. Let's imagine that this type had been:
type ProductAutoSuggestQueryResponse = {
node: {
products: {
edges: readonly {
node: {
name: string;
id: string;
currencies: readonly string[];
};
}[];
};
};
}
Given a value v
of the modified version of ProductAutoSuggstQueryResponse
, you could get a value of the desired node type by reading, say, v.node.products.edges[0].node
. More generally, you could get such a value by reading v[p][q][r][i][s]
when p
is of type 'node'
and q
is of type 'products'
and r
is of type 'edges'
and i
is of type number
and s
is of type 'node'
. Such indexing can be done at the type level with indexed access types:
type NodeType =
ProductAutoSuggestQueryResponse['node']['products']['edges'][number]['node'];
/* type NodeType = {
name: string;
id: string;
currencies: readonly string[];
} */
Which is what you want.
So that's great. Unfortunately your ProductAutoSuggestQueryResponse
type is not exactly what I wrote above. At various levels through the object structure, there are optional or otherwise null
able properties, meaning that they can include undefined
or null
. Instead of {a: string}
, you've got {a: string} | undefined
or {a: string} | null
or {a: string} | undefined | null
. You can't safely index into something which might be undefined
or null
, so the compiler won't let you use indexed access types directly.
What you can do is use the NonNullable<T>
utility type to turn something like {a: string} | undefined | null
into {a: string}
before you index into it. This can be done manually, but it's annoying:
type NodeTypeManual = NonNullable<
NonNullable<
NonNullable<
NonNullable<
ProductAutoSuggestQueryResponse['node']
>['products']
>['edges']
>[number]
>['node']
/* type NodeTypeManual = {
name: string;
id: string;
currencies: ReadonlyArray<string> | null;
} */
If you find that you're going to do this often, you can write a recursive conditional type that operates on a tuple of indices:
type NonNullableDeepIndex<T, KT extends PropertyKey[]> =
KT extends [infer KF, ...infer KR] ? (
KF extends keyof NonNullable<T> ? (
NonNullableDeepIndex<NonNullable<T>[KF], Extract<KR, PropertyKey[]>>
) : never
) : NonNullable<T>;
The type function NonNullableDeepIndex
takes a type T
and a tuple of keys KT
. If the tuple of keys is empty, we just return NonNullable<T>
. Otherwise we grab the first key KF
and index into NonNullable<T>
with KF
, and take the new type and the rest of the key tuple KR
, and recursively evaluate NonNullableDeepIndex
.
Then we can express NodeType
a little less repetitively as:
type NodeType = NonNullableDeepIndex<
ProductAutoSuggestQueryResponse,
["node", "products", "edges", number, "node"]
>;
/* type NodeType = {
name: string;
id: string;
currencies: ReadonlyArray<string> | null;
} */