Comparing SomeType to SomeType[] - Which One Is Better?

Using a constant string value to narrow down a union type is simple and effective:

type Payload1 = { /* ... arbitrary type ... */ };
type Payload2 = { /* ... arbitrary type ... */ };
type T1 = { type: 'type1', payload: Payload1 }
type T2 = { type: 'type2', payload: Payload2 }
type T = T1 | T2;

const fn = (value: T) => {

    if (value.type === 'type1') {
        value; // TypeScript recognizes `value is T1`
    }

    if (value.type === 'type2') {
        value; // TypeScript recognizes `value is T2`
    }

};

Initially, there are only two cases to consider:

  1. value.type is the constant "type1"
  2. value.type is the constant "type2"

But when expanding T to allow payload to be a single item or an array, the possibilities increase to 4:

  1. value.type is "type1" and value.payload is not an array
  2. value.type is "type1" and value.payload is an array
  3. value.type is "type2" and value.payload is not an array
  4. value.type is "type2" and value.payload is an array

Here's an illustration:

type Payload1 = {};
type Payload2 = {};
type T1Single = { type: 'type1', payload: Payload1 }
type T1Batch = { type: 'type1', payload: Payload1[] };
type T2Single = { type: 'type2', payload: Payload2 }
type T2Batch = { type: 'type2', payload: Payload2[] };

// Updated T with 4 types:
type T = T1Single | T1Batch | T2Single | T2Batch;

const fn = (value: T) => {

    if (value.type === 'type1' && !Array.isArray(value.payload)) {
        value; // TypeScript indicates `value is T1Single | T1Batch` now
    }

    if (value.type === 'type1' && Array.isArray(value.payload)) {
        value; // TypeScript indicates `value is T1Single | T1Batch` now
    }

    if (value.type === 'type2' && !Array.isArray(value.payload)) {
        value; // TypeScript indicates `value is T2Single | T2Batch` now
    }

    if (value.type === 'type2' && Array.isArray(value.payload)) {
        value; // TypeScript indicates `value is T2Single | T2Batch` now
    }

};

Playground

Why is typescript only partially narrowing down the type, and how can I achieve fully narrowed values for the 4 cases?

UPDATE: It appears that using multiple conditions in the if statement does not help; TypeScript struggles with narrowing based on Array.isArray alone:

type Payload = {};
type Single = { payload: Payload }
type Batch = { payload: Payload[] };

const fn = (value: Single | Batch) => {

    if (!Array.isArray(value.payload)) {
        value; // TypeScript still shows `value is Single | Batch`
    }

    if (Array.isArray(value.payload)) {
        value; // TypeScript still shows `value is Single | Batch`
    }

};

Answer №1

When attempting to handle T as a discriminated union, it's important to note that the payload property is not identified as a discriminant. In order for a property to serve as a valid discriminant, it must consist of unit/literal types. While your type property is acceptable due to "type1" and "type2" acting as string literal types, arrays and your Payload types are considered object types rather than literal types. Therefore, attempting to validate value.payload will not effectively narrow the apparent type of value itself.

It's worth noting that Array.isArray(value.payload) acts as a type guard on the value.payload property, but does not propagate the narrowing up to value due to it not being a discriminant. There is an existing feature request at microsoft/TypeScript#42384 to enable property type guards to spread up to containing objects. However, this feature is not currently implemented as it was deemed costly to generate new types for every type guard check on a nested property.


For the time being, if you want to achieve similar behavior, you can create a custom type guard function that narrows a value based on whether its payload property is an array. Here's an example:

function hasArrayPayload<T extends { payload: any }>(
    value: T): value is Extract<T, { payload: any[] }> {
    return Array.isArray(value.payload)
}

Instead of directly using Array.isArray(value.payload), you can utilize hasArrayPayload(value):

const fn = (value: T) => {
    if (value.type === 'type1' && !hasArrayPayload(value)) {
        value; // (parameter) value: T1Single
    }

    if (value.type === 'type1' && hasArrayPayload(value)) {
        value; // (parameter) value: T1Batch
    }

    if (value.type === 'type2' && !hasArrayPayload(value)) {
        value; // (parameter) value: T2Single
    }

    if (value.type === 'type2' && hasArrayPayload(value)) {
        value; // (parameter) value: T2Batch
    }
};

Playground link to code

Answer №2

It is impossible to simultaneously categorize multiple types in one go.

Instead, you can utilize type predicates or type guards.

For instance:

type Payload1 = { action: string };
type Payload2 = { action: number };
type T1 = { type: "type1"; payload: Payload1 };
type T2 = { type: "type2"; payload: Payload2 };
type T3 = { type: "type1"; payload: Payload1[] };
type T4 = { type: "type2"; payload: Payload2[] };
type T = T1 | T2 | T3 | T4;

const isType1 = (arg: T1 | T2 | T3 | T4): arg is T1 | T3 => {
  return arg.type === "type1";
};
const isTypeArray = <T>(arg: T | T[]): arg is T[] => {
  return Array.isArray(arg);
};

Check out the codesandbox link for the example above: playground

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

Issues with Cross-Origin Resource Sharing (CORS) have been identified on the latest versions of Android in Ionic Cordova. However, this problem does not

I am encountering an issue with my TypeScript Ionic code. It works well in browsers and some older mobile devices, but it fails to function on newer Android versions like 8+. I need assistance in resolving this problem. import { Injectable } from '@an ...

Handling errors in nested asynchronous functions in an express.js environment

I am currently developing a microservice that sends messages to users for phone number verification. My focus is on the part of the microservice where sending a message with the correct verification code will trigger the addition of the user's phone n ...

The type 'never' does not have a property named 'map'

Whenever I try to make an axios get request within a next.js getServerSideProps function, I encounter a persistent TypeScript error underline on the map method. Despite troubleshooting extensively, I have not been able to resolve it. The request successf ...

Ways to avoid using a specific type in TypeScript

Imagine having a class that wraps around a value like this: class Data<T> { constructor(public val: T){} set(newVal: T) { this.val = newVal; } } const a = new Data('hello'); a.set('world'); // typeof a --> Primitiv ...

I'm encountering a Typescript error where I'm unable to assign a function to RefObject.current and it's indicating that the function is not callable

Does anyone know why assigning a function type to a ref.current type is causing me issues? useEffect(() => { savedHandler.current = handler; // ERROR HERE: }, [handler]); TS2741: Property 'current' is missing in type '(e: Chang ...

Issue connecting to Oracle database: Unable to access properties of undefined (attempting to read '_getConnection')

Encountering an issue with node oracle connection. It was successfully connected before in the same application, but now it is not working after updating the node version. The connection string seems fine as the same connection is working in another appli ...

When I select a checkbox in Angular 2, the checkall function does not continue to mark the selected checkbox

How can I check if a checkbox is already marked when the selectAll method is applied, and then continue marking it instead of toggling it? selectAll() { for (let i = 0; i < this.suppliersCheckbox.length; i++) { if (this.suppliersCheckbox[i].type == " ...

Incorrect line numbers displayed in component stack trace [TypeScript + React]

Challenge I am currently working on integrating an error boundary into my client-side React application. During development, I aim to showcase the error along with a stack trace within the browser window, similar to the error overlays found in create-reac ...

Encountering an error in Angular 8 where attempting to access an element in ngOnInit results in "Cannot read property 'focus' of null"

My html code in modal-login.component.html includes the following: <input placeholder="Password" id="password" type="password" formControlName="password" class="form-input" #loginFormPassword /> In m ...

Angular 1.5 Karma unit test causes duplicate loading of ng-mock library

My current web app is built using Typescript 2.4.2 and compiled with the latest Webpack version (2.7.0). I am in the process of incorporating Karma tests utilizing Jasmine as the assertion library. Below is my karma configuration file: 'use strict& ...

Error Message: The Reference.update operation in Angular Firebase failed due to the presence of undefined value in the 'users.UID.email' property

Having recently started to use the Firebase database, I encountered an issue while trying to update the UID to the Realtime Database during signup. The error message displayed was: Error: Reference.update failed: First argument contains undefined in prop ...

Typescript Syntax for Inferring Types based on kind

I'm struggling to write proper TypeScript syntax for strict type inference in the following scenarios: Ensuring that the compiler correctly reports any missing switch/case options Confirming that the returned value matches the input kind by type typ ...

Type Assertion for Optional Values in TypeScript

Just confirming the proper way to handle situations like this. My current setup involves using Vue front-end with Typescript, sending data to an API via axios. I've defined reactive objects as follows: const payload = reactive({ name: '' ...

Adding existing tags to Select2 in Angular2 can be accomplished by following these steps:

HTML: <select data-placeholder="Skill List" style="width:100%;" class="chzn-select form-control" multiple="multiple"> <option *ngFor="#skill of allSkills" [ngValue]="skill">{{skill}} </option> </select> TS: allSkills = [& ...

Error: Attempting to access 'pageContext' property on undefined object, resulting in TypeError while utilizing sp pnp v3

I am currently following a tutorial to build a webpart using SPFX and SP/PNP v3: https://learn.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/use-sp-pnp-js-with-spfx-web-parts I have also consulted: Here is the main .ts file: public async onIn ...

What is the process of mapping in a React Element?

I have encountered an issue while trying to implement my parameter, which is an array of objects. The error message I received states: Parameter 'option' implicitly has an 'any' type.ts(7006) I am unable to determine the cause of this ...

Leverage one Injectable service within another Injectable service in Angular 5

I am facing difficulties in utilizing an Injectable service within another Injectable service in Angular 5. Below is my crudService.ts file: import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; ...

How to use TypeScript to print a blob (PDF) in Internet Explorer or Microsoft Edge?

I need to print a pdf blob using typescript. I've tried the following code, which works in Chrome but not in Edge. Code 1 (works in Chrome but prints blank in Edge) - const fileURL = URL.createObjectURL(blob); const iframe = document.createE ...

How to stop a method in Angular2 when a specific response is received?

I've been grappling with the idea of unsubscribing from a method in Angular2 once it receives a specific response. settings.component.ts Within my component, the method in question is connectToBridge, where the value of this.selectedBridge is a stri ...

Saving a boolean value and a number to Firestore in an Angular application

In my Angular 5 typescript project, I have a form with various input fields and selections. Here is how I am capturing the form values: let locked: boolean = (<HTMLInputElement>document.getElementById("locked")).value; let maxPlayers: number = (& ...