T.Chmelevskij
T.Chmelevskij

Reputation: 2139

Dealing with nested possibly null values in Typescript

I am using GraphQL and auto generated types for my queries with Typescript. It tends to create quite a few nested type | null types. So I need to do quite long checks for non null values. E.G.

            data &&
            data.getCoach &&
            data.getCoach.workouts &&
            data.getCoach.workouts.items &&
            data.getCoach.workouts.items.map((workout) => {/* do something*/});

I've tried using lodash has helper to check for path existence like

has('getCoach.workouts.items', data) &&
data.getCoach.workouts.items.map((workout) => {/* do something*/});

but ts compiler doesn't seem to understand this.

Is there better way for checking this apart from making my GraphQL endpoint always return the value?

Upvotes: 3

Views: 4352

Answers (2)

Dean
Dean

Reputation: 948

Edit:

Optional chaining has been released in typescript 3.7!

You can now do:

const items = data?.getCoach?.workouts?.items

items && items.map((v) => {
    console.log(v)
})

Previous answer:

Just thought I'd mention that optional chaining has made it to stage 2 at TC39!

I don't think there are any neat solutions at the moment, so something like jcalz's answer might have to do for now.

Hopefully TS introduces it early like they did with async/await 🤞and I can update this answer.

Upvotes: 3

jcalz
jcalz

Reputation: 327884

TypeScript isn't quite powerful enough to represent what you're trying to do. Ideally, JavaScript and TypeScript would feature a null-coalescing operator and you'd use that. But it doesn't have that.

If you try to strongly type the lodash has method, you'll find that the compiler cannot concatenate string literal types or perform regular expression operations on them, so there's no way for the compiler to take the string 'getCoach.workouts.items' and understand that getCoach will be a key of data, and that workouts will be a key of data.getCoach, etc.

Even without using has, it is tricky to represent the sort of nested type manipulation involved here without running into issues with recursion of the sort that makes the compiler angry or crashy.

The best I can do is: wrap the object in a Proxy (requires ES2015 or later) which always has a value at any key you want to use, and you use a special key name (such as "value") to pull out the actual value of the property, or undefined if there isn't one. Here's the implementation:

type WrapProperties<T, K extends keyof any> = Record<K, T> & { [P in keyof T]-?: WrapProperties<T[P], K> }

function wrapProperties<T>(val: T): WrapProperties<T, "value">;
function wrapProperties<T, K extends keyof any>(val: T, valueProp: K): WrapProperties<T, K>;
function wrapProperties(val: any, valueProp: keyof any = "value"): WrapProperties<any, any> {
    return new Proxy({}, {
        get: (_, p) => p === valueProp ? val : wrapProperties(
            (typeof val === 'undefined') || (val === null) ? val : val[p], valueProp
        )
    });
}

So now, instead of this:

if (data && data.getCoach && data.getCoach.workouts && data.getCoach.workouts.items) {
  data.getCoach.workouts.items.map((workout) => {/* do something*/}) 
}

You should be able to do this:

const wrappedData = wrapProperties(data, "val");    
if (wrappedData.getCoach.workouts.items.value) {
  wrappedData.getCoach.workouts.items.value.map((workout) => {/* do something*/}) 
}

I don't know if the implementation is perfect; it seems to work when I try it. As with anything you get from Stack Overflow, your mileage may vary and caveat emptor. Maybe it will help you. Good luck!

Upvotes: 1

Related Questions