effect icon indicating copy to clipboard operation
effect copied to clipboard

[ReadOnlyArray] Array.isEmptyArray does not perform type narrowing

Open samuelebonini opened this issue 2 years ago • 2 comments

Given a variable arr of type T[], I'm noticing that

import * as Array from "@effect/data/ReadonlyArray"

if(Array.isEmptyArray(arr)) {
    return;
}
arr; // <-- here arr still has type T[]

whereas

import * as Array from "@effect/data/ReadonlyArray"

if(!Array.isNonEmptyArray(arr)) {
    return;
}
arr; // <-- here arr has type [T, ...T[]]

Am I missing something? Shouldn't the first snippet also exhibit the same behavior, i.e. narrow the type down to a non-empty array?

samuelebonini avatar Aug 23 '23 15:08 samuelebonini

This is a TypeScript issue AFAIK. Refinement types are good at telling what a type is, but not good at what it isn't, unless the type you're refinining is a union. However, in this example, we're dealing subtypes of Arrays rather than unions.

EDIT: For example, we have similar kinds of code at my job: Screenshot 2023-08-23 at 12 32 46 PM

TylorS avatar Aug 23 '23 16:08 TylorS

You can get the desired type safety at a cost of double negation (which I wouldn't trade for)

import { isNonEmptyArray } from '@effect/data/ReadonlyArray'


function foo<T>(arr: T[]) {
    if (!isNonEmptyArray(arr)) {
        return;
    }

    arr; // [T, ...T[]]
}

Or if you can live with nesting

import { isNonEmptyArray } from '@effect/data/ReadonlyArray'

function foo<T>(arr: T[]) {
    if (isNonEmptyArray(arr)) {
      arr; // [T, ...T[]]
    }

}

nandin-borjigin avatar Sep 14 '23 16:09 nandin-borjigin

Unfortunately we can't fix this, as typescript only allows you to narrow based on what the type is, not what it isn't. Will close for now.

tim-smart avatar Mar 19 '24 21:03 tim-smart