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
}
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.