Home > Back-end >  How to pull null check into function in typescript
How to pull null check into function in typescript

Time:12-16

I have some code where I have several functions that need to null check an object then do stuff with it, or throw an error if it's null, like so:

interface SomeObject {
    doThingOne: () => string;
    doThingTwo: () => string;
}

let object: SomeObject | null; 

function doThingOne() {
  if(!object) {
    throw new Error('Object is null!');
  }
  // Typescript knows that object is not null, so it's fine with accessing its properties/methods
  return object.doThingOne();
}

function doThingTwo() {
  if(!object) {
    throw new Error('Object is null!');
  }
  return object.doThingTwo();
}

I'd like to pull out the null check into a function to reduce the duplicated code, like so:

interface SomeObject {
    doThingOne: () => string;
    doThingTwo: () => string;
}

let object: SomeObject | null; 

function requireObject() {
  if(!object) {
    throw new Error('Object is null!');
  }
}

function doThingOne() {
  requireObject();
  // Typescript no longer knows that object is defined, so it gets mad at the next line
  return object.doThingOne();
}

function doThingTwo() {
  requireObject();
  return object.doThingTwo();
}

However, when I do so, typescript is no longer aware that after the check, object definitely exists. Is there a clean way to do this?

This question seems similar, but wouldn't actually let me save on much code repetition as I'd still need to set up an if

CodePudding user response:

There's no way to write requireObject() and have that affect the type of object, since the compiler does not see requireObject() as related to object in any way. (If the compiler could manage all possible state changes due to mutation of closed-over variables inside functions, that would be great, but it's just not computationally feasible. See microsoft/TypeScript#9998 for the general problem.)

What you can do is pass object into requireObject() and annotate that requireObject() is an assertion function. Assertion functions must be implemented as void-returning (so they don't return anything), and the annotated return type is an "assertion predicate" of the form asserts x is Y (where x is the name of one of the function parameters) or just asserts x if you want to say that x is truthy.

So that gives us:

function requireObject(x: any): asserts x {
  if (!x) {
    throw new Error('something is falsy here');
  }
}

And now we can do this without error:

let object: SomeObject | null;

function doThingOne() {
  requireObject(object);
  return object.doThingOne(); // okay
}

function doThingTwo() {
  requireObject(object);
  return object.doThingTwo(); // okay
}

Playground link to code

CodePudding user response:

You could use the ! operator to tell the compiler that object will definitely contain a value.

return object!.doThingOne();

I try to avoid that sort of thing. It's easy to get into trouble later on. Instead, I would define the requireObject function to always return an object.

function requireObject(): object {
    if (!object) {
        throw new Error('Object is null!');
    }
    return object;
}

You can then call it like this:

object = requireObject();

Since requireObject always returns an object type, the compiler will know that at that point object is not null.

The only problem now is, because object is actually a TypeScript type, the code is a little confusing. It should work (I haven't actually tried it), but you may want to consider naming the variable something else.

  • Related