1

For example I have a method isEmpty that checks if anything is empty, null, or undefined and returns true if so.

However in TypeScript it doesn't let the interpreter know that this is the case and I get a red underline in my IDE (WebStorm)

Example code

let str: string | undefined = undefined
if (!isEmpty(str)) {
   doSomeWorkFunction(str) // this line is shows the error 'string | undefined is not assignable to type string
}

However if the code is

let str: string | undefined = undefined
if (str) {
    doSomeWorkFunction(str) // no error because the interpreter knows I checked the value
}

The fix I would like to avoid is

let str: string | undefined = undefined
if (!isEmpty(str)){
    // @ts-ignore
    doSomeWorkFunction(str) // no error since ts is now ignoring this error
}

How might I go about still keeping the TypeScript strict null checks in place without having to ignore issues like this.

LazyOne
  • 158,824
  • 45
  • 388
  • 391
mjwrazor
  • 1,866
  • 2
  • 26
  • 42

1 Answers1

2

TypeScript has a feature called "type guards" that helps in this situation: https://www.typescriptlang.org/docs/handbook/advanced-types.html. Specifically, it lets you tell the compiler that the return type is not just a boolean, but a boolean that means something specific about the types of the inputs. For example, you can convert a function like this

function isDefinedString(input: string | undefined): boolean

into a function like this:

function isDefinedString(input: string | undefined): input is string

The return type is still a boolean, but now the compiler will assume that the input is specifically a string and not any other type allowed by the argument declaration (in this case undefined).

Try using this signature on your existing isEmpty function declaration. Although not required to make it work, because you are adding this additional context to the function signature I'd recommend changing the name of isEmpty to reflect its dual purpose of checking emptiness and whether the variable is defined.

Edit: One caveat to returning type information is that returning false will make the compiler assume that the object is not that type. In the above example, if isDefinedString returns false then the compiler will assume that it is not a string. This runs into problems with any or generic parameters, because returning false effectively tells the compiler that there is no type (or in the compiler's words, there is "never" a type) that satisfies your criteria. While this doesn't result in an error directly, the fact that the compiler has no type that works with your object means you can't do anything meaningful with the object in the if/else branch triggered by your type guard returning false. As such, if you are using a broad type such as any or a generic, you will want to limit what your type guard says to something like input is (null | undefined) or input is MySpecificInterface if you plan to do something meaningful in both true and false cases. This trickiness may also be a sign that you want to separate your validation into two checks:

if(typeGuard(myObject)) {
  if(isValid(myObject)) {
    // do something with valid object
  } else {
    // do something with invalid object
  }
}
// do nothing without an object to act upon
Tim Ernsberger
  • 595
  • 4
  • 6
  • What if the input type is any or for example where the input is unknown – mjwrazor Jul 28 '21 at 20:09
  • I was able to get it to work by adding in the basic types as the input plus the generic on the outside `isEmpty( obj: T | string | number | bigint | boolean | undefined | null ): obj is T ` – mjwrazor Jul 28 '21 at 20:17
  • Actually it has issue with checking things saying that I can't use that item I just checked because it is 'never' How might I fix this? – mjwrazor Jul 28 '21 at 21:18
  • It sounds like the compiler is having trouble figuring out what types could match `T`. In order to help I'd need to see the code that is causing the issue. I'm not sure if a generic type is the right thing to use in this case. How do you define whether any type is empty? It makes sense for a string or array, but a boolean can't be "empty". If you only need to check if an object is defined/non-null, then you can use something simple like `myObject != null`. If you have specific validation that only applies to some types, then you can use a type guard. – Tim Ernsberger Jul 28 '21 at 21:37
  • I found the answer https://github.com/microsoft/TypeScript/issues/27041#issuecomment-420769327 which has me do this `function isEmpty( obj: T | null | undefined ): obj is (null | undefined)` if you could explain on this I can accept your answer. using this as the internals https://stackoverflow.com/a/28953167/4807889, By explain just adding in this info to your answer since you lead me in the right direction. – mjwrazor Jul 28 '21 at 21:46