Determine the data types present in an array using TypeScript

It appears that Typescript has a strong compatibility with AST. When checking x.type == "Abc", Typescript is able to infer that x is of type Abc. This capability comes in handy when I use it for typechecking JS files with JSDOC format annotations, and I believe it also applies to pure Typescript files.

However, I'm facing some challenges when testing for an array of elements.

The first example functions correctly because each element is looped over individually, and only added to the array if the type check passes. Consequently, Typescript accurately identifies the return type of the function as Property[].

/**
 * @param {ObjectExpression} objectAst
 */
function getPropertiesList(objectAst) {
    let propertiesList = []
    for (let p of objectAst.value.properties) {
        if (p.type == "Property")
            propertiesList.push(p)
        else
            throw new Error("Properties field has elements that aren't of type `Property`")
    }
    return propertiesList
}

On the other hand, the second example, which is functionally equivalent but cleaner and avoids creating a new array, does not work as expected. The inferred type is

(SpreadElement|Property|ObjectMethod|ObjectProperty|SpreadProperty)[]
, indicating that the type check is not taken into consideration.

/**
 * @param {ObjectExpression} objectAst
 */
function getPropertiesList(objectAst) {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every(p => p.type == "Property"))
        throw new Error("Properties field has elements that aren't of type `Property`")
    return propertiesList
}

Could someone shed some light on why Typescript handles one scenario differently from the other?

While Typescript can leverage checks to refine a type (as demonstrated in the first example), it seems unable to apply these checks on arrays.

Should this be considered a limitation or potential bug in the Typescript compiler since both code snippets should logically return the same type?

EDIT: For context and ease of testability, I have imported types from recast using the following syntax:

/**
 * @typedef { import('recast').types.namedTypes.ObjectExpression} ObjectExpression 
 * @typedef { import('recast').types.namedTypes.Property} Property 
*/

Answer №1

The problem lies in the fact that the compiler fails to recognize that array.every() can serve as a type guard for the type of array. Additionally, the callback function p => p.type == "Property" is not identified as a type guard for the type of p. While the compiler does well with analyzing inline code for potential type narrowing, it struggles when control flow enters functions.

If you want TypeScript to acknowledge that invoking boolean-returning functions acts as a type guard, you must explicitly declare such functions as a user-defined type guard. A function like foo(x: T): boolean should be modified to foo(x: T): x is U, where "x is U" serves as a type predicate. If foo(val) returns true, then the compiler will narrow val to U.

To update the callback, change p => p.type == "Property" to

(p): p is Property => p.type == "Property"
. As for array.every(), this method is defined in the standard library within the Array<T> interface. Fortunately, you can merge additional method overloads into interfaces. However, if your code resides in a module, you may need to utilize global augmentation to extend global interfaces like Array<T>.

interface Array<T> {
    every<U extends T>(cb: (x: T) => x is U): this is Array<U>;
}

Now, if the callback functions as a type-guard function, the compiler will recognize that every() itself serves as a type guard. Consequently, your code will operate as intended:

function getPropertiesList(objectAst: ObjectAST): Property[] {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every((p): p is Property => p.type == "Property"))
        throw new Error("Properties field contains elements that are not of type `Property`")
    return propertiesList
}

For a single usage of every(), this might seem like an excessive amount of effort. In reality, it's often more practical to employ a type assertion and proceed. Type assertions come in handy when you possess greater knowledge about types than the compiler does, making them suitable for scenarios like this:

function getPropertiesListAssert(objectAst: ObjectAST): Property[] {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every(p => p.type == "Property"))
        throw new Error("Properties field contains elements that are not of type `Property`")
    return propertiesList as Property[]; // assert
}

Hoping this information proves beneficial to you; best of luck!

Playground Link to 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

Dealing with Typescript (at-loader) compilation issues within a WebPack environment

Currently, I am using Visual Studio 2017 for writing an Angular SPA, but I rely on WebPack to run it. The current setup involves VS building the Typescript into JS, which is then utilized by WebPack to create the build artifact. However, I am looking to t ...

What is the method for executing a custom command within the scope of a tree view item?

I am trying to implement a custom "ping" function in my VS Code Extension that will send the details of a selected treeview object. Despite looking at various examples, I have been unable to properly build and register the command. Although I can invoke th ...

Tips for creating a custom script in my React Native application

My React Native app requires a script to generate static files during the release process. The app is a game that utilizes pre-computed boards, which are resource-intensive to compute. Therefore, I am developing a script that will create these boards and s ...

Trying out deletion of items from a React-managed list using Cypress testing

A React list was developed to allow users to delete items by clicking a button. The deletion process is implemented as follows: const handleRemove = (index: number) => { onChange(fieldName, (prevState) => { return { ...prevState, ...

Obtaining the TemplateRef from any HTML Element in Angular 2

I am in need of dynamically loading a component into an HTML element that could be located anywhere inside the app component. My approach involves utilizing the TemplateRef as a parameter for the ViewContainerRef.createEmbeddedView(templateRef) method to ...

The NGRX state spread operator requires the Type to include a '[Symbol.iterator]()' function

Utilizing NGRX Entity adapter for state initialization has been encountering an issue, specifically with the getInitialState method. export const initialState = adapter.getInitialState({ eventsError: null, eventsLoading: false }); ex ...

Acquire the property that broadens the interface

I'm currently working on a TypeScript function that aims to retrieve a property from an object with the condition that the returned property must extend a certain HasID interface. I envision being able to utilize it in this manner: let obj = { foo ...

Tips for resolving the issue with the 'search input field in the header' across all pages in angular 5 with typescript

I currently have a search field in the header that retrieves a list of records when you type a search term and click on one of them. The search function utilizes debounceTime to reduce API requests, but I'm encountering an issue where the search doesn ...

Ways to access nested keys in a TypeScript object as well as an array containing objects

As I develop a form generator, my goal is to achieve type safety for a nested object and an array of objects. Specifically, I want the 'name' property to correspond to the key of the respective object it belongs to. For instance, in the scenario ...

Dealing with server-side errors while utilizing react-query and formik

This login page utilizes formik and I am encountering some issues: const handleLogin = () => { const login = useLoginMutation(); return ( <div> <Formik initialValues={{ email: "", password: "" }} ...

Methods for showcasing an angular object generated by a function

There is a function in my code that returns an object. public getLinkedTREsLevel() { let result: any; if (this.entry && this.entry.config ) { this.entry.config.forEach( element => { if (element.name === 'creationTIme') { ...

Converting a string to a number is not functioning as expected

I am facing a problem with an input shown below. The issue arises when trying to convert the budget numeric property into thousands separators (for example, 1,000). <ion-input [ngModel]="project.budget | thousandsSeparatorPipe" (ngModelChange)="projec ...

Hand over the component method as an argument to a class

One of my components, called First, is responsible for creating a new instance of a Worker class. During the creation process of this class, I intend to pass the Read method as a callback method. Once this class completes its task, it will then invoke thi ...

Exploring the realm of Typescript custom decorators: The significance behind context

I'm currently working on a custom decorator that will execute decorated functions based on RxJS events. Everything seems to be going well so far, but I'm facing an issue when the function is executed: the context of the this object is lost. I&a ...

Create a line break in the React Mui DataGrid to ensure that when the text inside a row reaches its maximum

I'm facing an issue with a table created using MUI DataGrid. When user input is too long, the text gets truncated with "..." at the end. My goal is to have the text break into multiple lines within the column, similar to this example: https://i.sstati ...

The file transfer functionality in object FileTransfert is malfunctioning on certain Android devices when attempting to upload

A mobile application was created to facilitate the sharing of items. Users are required to provide information about the item they are sending, along with the option to add a picture of the object. To achieve this functionality, plugins such as cordova f ...

Validation of object with incorrect child fields using Typeguard

This code snippet validates the 'Discharge' object by checking if it contains the correct children fields. interface DischargeEntry { date: string; criteria: string; } const isDischargeEntry = (discharge:unknown): discharge is DischargeEntry ...

Utilizing TypeScript 2's Absolute Module Paths

The issue at hand: I am facing a challenge with relative module paths and have attempted to resolve it by configuring the baseUrl setting in my tsconfig.json file. Despite my efforts, I keep receiving an error indicating that the module cannot be found. I ...

TypeScript's version of Java's enum (or C#'s structure)

I'm facing the challenge of creating an enum in Typescript that mimics the functionality of Java enums. In TypeScript, only integer-based enums like C# are supported, unlike in Java where we can have custom objects with non-integer related properties ...

Struggling to send API POST request using Next.js

I'm relatively new to backend development, as well as Next.js and TypeScript. I'm currently attempting to make a POST request to an API that will receive a formData object and use it to create a new listing. My approach involves utilizing Next.js ...