Optional chaining for typescript types

825 views Asked by At

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:

  1. All those values in the middle are nullable
  2. I don't have an unique name for the error in my auto-generated types

I tried the following:

  1. works, but it is really hard to read:
type Error = NonNullable<NonNullable<NonNullable<MyQuery>['system']>['errors']>['list'][number]
  1. Doesn't work (?.['field'] also doesn't work)
type Error = MyQuery?['system']?['errors']?['list']?[number]
  1. Works but creates unnecessary variable:
const error = queryResult?.system?.errors?.list?.[0]
type Error: typeof error
  1. Kinda works, but fields inside Error also become not null which I don't want
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>

1

There are 1 answers

0
Oleg Valter is with Ukraine On

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