Reputation: 14739
I have a data structure that is deeply nested and I want to be able to reference an inner type in it, but that type doesn't have its own name/definition. For example:
MyQuery['system']['errors']['list'][number]
I auto-generate the MyQuery
type from a graphql query using graphql-codegen. I want the type of a single error
, but there are two problems:
I tried the following:
type Error = NonNullable<NonNullable<NonNullable<MyQuery>['system']>['errors']>['list'][number]
?.['field']
also doesn't work)type Error = MyQuery?['system']?['errors']?['list']?[number]
const error = queryResult?.system?.errors?.list?.[0]
type Error: typeof error
import { DeepNonNullable } from 'utility-types'
type Error = DeepNonNullable<MyQuery>['system']['errors']['list'][number]
Basically what I am asking is if there is an easier way to do "optional chaining for types" in typescript. My API is very null-prone and it would be very useful if I could do this more easily than using several NonNullable<T>
Upvotes: 11
Views: 1240
Reputation: 10365
if there is an easier way to do "optional chaining for types"
No, unfortunately, as of yet there is no native way to "optionally chain" deeply nested types. There is, however, quite a roundabout way of emulating that with a complex recursive conditional generic type and paths. First, you would need a reusable helper for handling index signatures:
type _IndexAccess<T, U extends keyof T, V extends string> = V extends "number"
? Exclude<T[U], undefined> extends { [x:number]: any } ?
Exclude<T[U], undefined>[number]
: undefined
: V extends "string" ?
Exclude<T[U], undefined> extends { [x:string]: any } ?
Exclude<T[U], undefined>[string]
: undefined
: V extends "symbol" ?
Exclude<T[U], undefined> extends { [x:symbol]: any } ?
Exclude<T[U], undefined>[symbol]
: undefined
: undefined;
Then you can create a helper type for recursively traversing the nested type relying on infer
and template literal types to process the path:
type DeepAccess<T, K extends string> = K extends keyof T
? T[K]
: K extends `${infer A}.${infer B}`
? A extends keyof T
? DeepAccess<Exclude<T[A], undefined>, B>
: A extends `${infer C}[${infer D}]`
? DeepAccess<_IndexAccess<T, C extends keyof T ? C : never, D>, B>
: undefined
: K extends `${infer A}[${infer B}]`
? A extends keyof T
? B extends keyof T[A]
? T[A][B]
: _IndexAccess<T, A, B>
: undefined
: undefined;
It's not pretty, but it allows for elegantly lensing into nested types:
type MyQuery = {
system?: {
errors?: {
list?: [{
answer: 42,
questions: { known: false }[]
}]
}
}
};
// false
type t1 = DeepAccess<MyQuery, "system.errors.list[number].questions[number].known">;
// [{ answer: 42; questions: { known: false; }[]; }] | undefined
type t2 = DeepAccess<MyQuery, "system.errors.list">;
// 42
type t3 = DeepAccess<MyQuery, "system.errors.list[number].answer">;
// undefined
type t4 = DeepAccess<MyQuery, "system.errors.list.unknown">;
Upvotes: 6