If you're wondering about the best way to specify that the return type of `myIsEmpty` functions as a type predicate to narrow the type of the `input` argument, you'll want `myIsEmpty` to serve as a user-defined type guard function.
When you make checks or adjustments to values of specific types, the compiler utilizes control flow analysis to assign more precise types to these values:
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; // works fine
}
As shown in the last line of the function, the compiler deduces that `input` is undoubtedly a `string[]` and not an `undefined[]`. It eliminates the possibility of `input` being `undefined` because the function would have already `return`ed in that scenario. This precision in type narrowing is exactly what you aim for.
However, control flow analysis does not extend across function calls. Refer to ms/TS#9998. Attempting to track such details would be too costly. So, relocating an inline check from `exampleInline()` to a standalone `myIsEmpty()` function impedes the narrowing process, which is counterproductive.
This is where user-defined type guard functions come into play. Hence, you need one of those.
Note that control flow analysis takes place during the assignment of a value to a variable of a union type. For instance, consider the following code snippet:
const input: string[] | undefined = undefined;
This expression automatically informs the compiler that `input` is and will always be `undefined`, resisting any test — inline or otherwise — to persuade the compiler otherwise. At most, you can conduct a test to convince the compiler that `input` signifies the impossible `never` type. Hence, in all these instances, I altered the code so that `input` is not initialized with a narrowed value, turning it into a function parameter instead.
Therefore, the ideal scenario involves keeping `input` un-narrowed initially and relying on `myIsEmpty(input)` to serve as a type guard function for `input`.
Regrettably, there is not a flawless alignment between what your function defines as "empty" and the types that TypeScript can efficiently represent in such a narrowing process. So you may resort to this approach:
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;
}
Nevertheless, you must then establish how to define `Empty`. Clearly, `undefined` and `null` represent emptiness, as does a zero-length array expressed as `[]`, the empty tuple type. Additionally, consider entities that equate to "false," such as literal types `false`, numeric literals `0` and `0n`, and the eccentric string comparisons that evaluate to `false`. Although TypeScript lacks a specific type for such strings, for simplicity, I consider the empty string `""` and the single-character string `"0"` as empty, disregarding everything else:
type Empty = undefined | null | "" | false | 0 | "0" | 0n | []
If you apply this definition, things mostly align with your requirements, especially for your sample use case:
function example(input: string[] | undefined): boolean {
if (myIsEmpty(input)) {
return true;
}
return input.length > 0; // works fine
}
This setup incurs no errors. Examining its operation, when `myIsEmpty(input)` returns `true`, the compiler refines `string[] | undefined` to simply `undefined`. Technically, it should have altered it to `[] | undefined` instead, since the empty array remains a possibility. Unfortunately, this discrepancy is a TypeScript anomaly reported in ms/TS#31156. Therefore, you might encounter challenges in this realm concerning the functionality of a type guard function. Nonetheless, since the specifics in the `true` scenario are inconsequential, this specific example showcases no adverse behavior.
Conversely, if `myIsEmpty(input)` returns `false`, the compiler narrows `string[] | undefined` to `string[]`, thereby permitting you to confidently assess `input.length`. Success!
Link to Playground for the code