How can I write my own isEmpty function so the typescript compiler knows a false result means the parameter IS defined?
The quick answer to this question is that you want to annotate that myIsEmpty
's return type is a type predicate that serves to narrow the type of the input
argument; that is, you want myIsEmpty
to be a so-called user-defined type guard function.
When you perform checks or modifications on values of certain types, the compiler will use control flow analysis to give these values more specific types:
function exampleInline(input: string[] | undefined): boolean {
if (input === undefined || input === null) {
return true;
}
if (input instanceof Array && input.length === 0) {
return true;
}
return input.length > 0; // okay
}
Here the compiler knows that in the last line of the function, input
is definitely a string[]
and not undefined[]
. It has eliminated the possibility that input
might be undefined
because knows that in such a case, the function would have already return
ed. That's exactly what you want.
But control flow analysis does not cross function call boundaries. See ms/TS#9998. It would be way too expensive to try to follow such things. So by taking an inline check and moving it out to a myIsEmpty()
function will prevent the narrowing from happening. Which is not what you want.
That's why user-defined type guard functions exist. So you want one of those.
Also note that control flow analysis works upon assignment of a value to a variable of a union type. So any example code that looks like
const input: string[] | undefined = undefined;
is automatically telling the compiler that input
is and will forever remain undefined
, and no test, inline or otherwise, will convince the compiler that input !== undefined
. At best you can perform a test that convinces the compiler that input
is the impossible never
type. So in all these examples I've changed things so that input
is not initialized with a narrowed value, by making it a function parameter.
Okay, so we want input
to be non-narrowed to start with, and we want myIsEmpty(input)
to act as a type guard function on input
.
Unfortunately there's not a perfect match between what your function deems as "empty" and types that TypeScript can straightforwardly represent in such a narrowing operation. So you can do this:
function myIsEmpty(input: unknown): input is Empty {
if (input === undefined || input === null) {
return true;
}
if (input instanceof Array) {
return input.length === 0;
}
return input == false;
}
but then you have to decide how to define Empty
. Clearly undefined
and null
are empty, and so is a zero-length array which can be represented as []
, the empty tuple type. And we have to consider things that compare as "equal to" false
, which is a bizarre menagerie indeed. There are the tame literal types like false
or the numeric literals 0
and its bigint cousin 0n
. You've thankfully eliminated arrays, since [[[0]]] == false
in JavaScript. But you've still left "strings that can be coerced to 0
", meaning that " \v \n -0.0e+10000 \n \r \t " == false
. Yuck. There is no specific TypeScript type corresponding to those. I'm going to give up and pay lip service to such strings by considering the empty string ""
and the one-character long "0"
string to be empty, and everything else we'll just fail to deal with:
type Empty = undefined | null | "" | false | 0 | "0" | 0n | []
If you use this definition things will mostly work as you want, at least for your example:
function example(input: string[] | undefined): boolean {
if (myIsEmpty(input)) {
return true;
}
return input.length > 0; // okay
}
There's no error here. Let's see what is going on with it. When myIsEmpty(input)
returns true
, the compiler narrows string[] | undefined
to undefined
. Now, technically, it should have narrowed to [] | undefined
instead; the empty array is still possible. This is unfortunately a bug in TypeScript, reported at ms/TS#31156. So again, you could easily run into some friction here with what a type guard function can do for you here. But since you don't really care about what happens in the true
case, this particular example doesn't show any bad behavior.
In the last line, where myIsEmpty(input)
must have returned false
, the compiler has narrowed string[] | undefined
to string[]
. And therefore you can check input.length
with impunity. Hooray!
Playground link to code