What is the best way to create a custom isEmpty function that communicates to the TypeScript compiler that a false result indicates the parameter is actually defined?

I have written the following code snippet:

export function myIsEmpty(input?: unknown): boolean {
  if (input === undefined || input === null) {
    return true;
  }
  if (input instanceof Array) {
    return input.length === 0;
  }
  return input == false;
}

Normally, I prefer using === over ==, but I hope this does not affect the validity of my question.

To test this function, I can create a passing unit test like this:

  it("empty", () => {
    expect(myIsEmpty(undefined)).toEqual(true);
    expect(myIsEmpty(null)).toEqual(true);
    expect(myIsEmpty([])).toEqual(true);
    expect(myIsEmpty('f')).toEqual(false);
    expect(myIsEmpty([1])).toEqual(false);
  });

The key point here is that if the parameter is "not empty," then it is considered defined.

However, the following code snippet does not compile:

function example(): boolean {
  const input: string[] | undefined = undefined;
  if (myIsEmpty(input)) {
    return true;
  }
  return input.length > 0; //error
}

https://i.sstatic.net/eAsur.png

Is there a way to modify my function so that Typescript can understand that !myIsEmpty(input) implies input !== undefined? If not, what is preventing the compiler from recognizing this automatically?

Answer №1

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Do Angular lifecycle hooks get triggered for each individual component within a nested component hierarchy?

I'm exploring the ins and outs of Angular lifecycle hooks with a conceptual question. Consider the following nested component structure: <parent-component> <first-child> <first-grandchild> </first-grandchild& ...

the process of accessing information from a service in an Angular Typescript file

After making a POST request using Angular's HTTP client, the response data can be accessed within the service. However, is there a way to access this data in the app.component.ts file? I am able to retrieve the response data within the service, but I ...

Presenting CSV data in a tabular format using Angular and TypeScript

I am facing an issue with uploading a CSV file. Even though the table adjusts to the size of the dataset, it appears as if the CSV file is empty. Could this be due to an error in my code or something specific about the CSV file itself? I'm still learn ...

Can one obtain a comprehensive array of interfaces or a detailed map showcasing all their variations?

I have developed a method that takes in an object containing data and returns an object that adheres to a specific interface. interface FireData { id: EventTypes; reason?: string; error?: string; } enum EventTypes { eventType1 = "ev1", ...

Customize the format of data labels in horizontal bar charts in NGX Charts

I am currently using ngx-charts, specifically the bar-horizontal module. My goal is to format the data labels and add a percentage symbol at the end. I attempted to use the [xAxisTickFormatting] property, but it seems that my values are located within the ...

ESLint's no-unused-vars rule is triggered when Typescript object destructuring is employed

I'm encountering an issue with my Typescript code where I am destructuring an object to extract a partial object, but it's failing the linter check. Here is the problematic code snippet: async someFunction(username: string): Promise<UserDTO> ...

Encountering a Javascript error while trying to optimize bundling operations

After bundling my JavaScript with the .net setting BundleTable.EnableOptimizations = true;, I've encountered a peculiar issue. Here's the snippet of the generated code causing the error (simplified): var somVar = new b({ searchUrl: "/so ...

Discovering the output type of a function within a template

My current project involves creating a template class called Binder that binds functions and parameters as a whole, distinguished by the returning type of the bound function. Here is my initial approach: template <typename return_type> class Binder ...

Exploring nested contexts in react testing library with renderHook

This is a sample code snippet in TypeScript that uses React and Testing Library: import React, { FC, useEffect, useMemo, useState } from 'react'; import { renderHook, waitFor } from '@testing-library/react'; interface PropsCtx { inpu ...

Troubleshooting data binding problems when using an Array of Objects in MatTableDataSource within Angular

I am encountering an issue when trying to bind an array of objects data to a MatTableDataSource; the table displays empty results. I suspect there is a minor problem with data binding in my code snippet below. endPointsDataSource; endPointsLength; endP ...

React hooks causing dynamic object to be erroneously converted into NaN values

My database retrieves data from a time series, indicating the milliseconds an object spends in various states within an hour. The format of the data is as follows: { id: mejfa24191@$kr, timestamp: 2023-07-25T12:51:24.000Z, // This field is dynamic ...

Issue with Adding Additional Property to react-leaflet Marker Component in TypeScript

I'm attempting to include an extra property count in the Marker component provided by react-leaflet. Unfortunately, we're encountering an error. Type '{ children: Element; position: [number, number]; key: number; count: number; }' is n ...

What is the correct way to initialize and assign an observable in Angular using AngularFire2?

Currently utilizing Angular 6 along with Rxjs 6. A certain piece of code continuously throws undefined at the ListFormsComponent, until it finally displays the data once the Observable is assigned by calling the getForms() method. The execution of getForm ...

What is the process of generating an instance from a generic type in TypeScript?

Managing data in local storage and making remote API queries are two crucial aspects of my service. When defining a data model, I specify whether it should utilize local storage or the remote API. Currently, I am working on creating a proxy service that c ...

Configuring the parameters property for a SSM Association in AWS CDK

I am working on utilizing the AWS Systems Manager State Manager to automate shutting down an RDS instance at 9PM using a cron job. Currently, I am constructing the CloudFormation template with the help of AWS CDK. While going through the AWS CDK documenta ...

What is the process for importing a map from an external JSON file?

I have a JSON file with the following configuration data: { "config1": { //this is like a map "a": [ "string1", "string2"], "b": [ "string1", "string2"] } } Previously, before transitioning to TypeScript, the code below worked: import ...

Issue with updating BehaviorSubject not being reflected when called from my service component has been identified

In my HomeComponent, I am currently using *ngIf to switch between 3 components. The focus right now is on the relationship between two of them - the ProductListComponent and the ProductDetailComponent. Inside the ProductListComponent, there is a ProductLis ...

Steps to customize the color scheme in your Angular application without relying on external libraries

Is there a way to dynamically change the color scheme of an Angular app by clicking a button, without relying on any additional UI libraries? Here's what I'm trying to achieve - I have two files, dark.scss and light.scss, each containing variabl ...

Oh no, an issue has occurred with The Angular Compiler! It appears that TypeScript version 3.9.10 was found instead of the required version, which should be >=3.6.4 and <

After upgrading my angular application from version 5 to version 9, I encountered an issue while trying to deploy my code on the server. ERROR in The Angular Compiler requires TypeScript >=3.6.4 and <3.9.0 but 3.9.10 was found instead. Even though ...

How should one properly format an array of objects with elements that are different types of variations?

I am currently developing a versatile sorting module using TypeScript. This module will take in an array of data objects, along with a list of keys to sort by. Once the sorting process is complete, the array will be sorted based on the specified keys. To ...